Adding skip scan (including MDAM style range skip scan) to nbtree

Started by Peter Geogheganover 1 year ago153 messages
2 attachment(s)

Attached is a POC patch that adds skip scan to nbtree. The patch
teaches nbtree index scans to efficiently use a composite index on
'(a, b)' for queries with a predicate such as "WHERE b = 5". This is
feasible in cases where the total number of distinct values in the
column 'a' is reasonably small (think tens or hundreds, perhaps even
thousands for very large composite indexes).

In effect, a skip scan treats this composite index on '(a, b)' as if
it was a series of subindexes -- one subindex per distinct value in
'a'. We can exhaustively "search every subindex" using an index qual
that behaves just like "WHERE a = ANY(<every possible 'a' value>) AND
b = 5" would behave.

This approach might be much less efficient than an index scan that can
use an index on 'b' alone, but skip scanning can still be orders of
magnitude faster than a sequential scan. The user may very well not
have a dedicated index on 'b' alone, for whatever reason.

Note that the patch doesn't just target these simpler "skip leading
index column omitted from the predicate" cases. It's far more general
than that -- skipping attributes (or what the patch refers to as skip
arrays) can be freely mixed with SAOPs/conventional arrays, in any
order you can think of. They can also be combined with inequalities to
form range skip arrays.

This patch is a direct follow-up to the Postgres 17 work that became
commit 5bf748b8. Making everything work well together is an important
design goal here. I'll talk more about that further down, and will
show a benchmark example query that'll give a good general sense of
the value of the patch with these more complicated cases.

A note on terminology
=====================

The terminology in this area has certain baggage. Many of us will
recall the patch that implemented loose index scan. That patch also
dubbed itself "skip scan", but that doesn't seem right to me (it's at
odds with how other RDBMSs describe features in this area). I would
like to address the issues with the terminology in this area now, to
avoid creating further confusion.

When I use the term "skip scan", I'm referring to a feature that's
comparable to the skip scan features from Oracle and MySQL 8.0+. This
*isn't* at all comparable to the feature that MySQL calls "loose index
scan" -- don't confuse the two features.

Loose index scan is a far more specialized technique than skip scan.
It only applies within special scans that feed into a DISTINCT group
aggregate. Whereas my skip scan patch isn't like that at all -- it's
much more general. With my patch, nbtree has exactly the same contract
with the executor/core code as before. There are no new index paths
generated by the optimizer to make skip scan work, even. Skip scan
isn't particularly aimed at improving group aggregates (though the
benchmark I'll show happens to involve a group aggregate, simply
because the technique works best with large and expensive index
scans).

My patch is an additive thing, that speeds up what we'd currently
refer to as full index scans (as well as range index scans that
currently do a "full scan" of a range/subset of an index). These index
paths/index scans can no longer really be called "full index scans",
of course, but they're still logically the same index paths as before.

MDAM and skip scan
==================

As I touched on already, the patch actually implements a couple of
related optimizations. "Skip scan" might be considered one out of the
several optimizations from the 1995 paper "Efficient Search of
Multidimensional B-Trees" [1]https://vldb.org/conf/1995/P710.PDF -- Peter Geoghegan -- the paper describes skip scan under
its "Missing Key Predicate" subsection. I collectively refer to the
optimizations from the paper as the "MDAM techniques".

Alternatively, you could define these MDAM techniques as each
implementing some particular flavor of skip scan, since they all do
rather similar things under the hood. In fact, that's how I've chosen
to describe things in my patch: it talks about skip scan, and about
range skip scan, which is considered a minor variant of skip scan.
(Note that the term "skip scan" is never used in the MDAM paper.)

MDAM is short for "multidimensional access method". In the context of
the paper, "dimension" refers to dimensions in a decision support
system. These dimensions are represented by low cardinality columns,
each of which appear in a large composite B-Tree index. The emphasis
in the paper (and for my patch) is DSS and data warehousing; OLTP apps
typically won't benefit as much.

Note: Loose index scan *isn't* described by the paper at all. I also
wouldn't classify loose index scan as one of the MDAM techniques. I
think of it as being in a totally different category, due to the way
that it applies semantic information. No MDAM technique will ever
apply high-level semantic information about what is truly required by
the plan tree, one level up. And so my patch simply teaches nbtree to
find the most efficient way of navigating through an index, based
solely on information that is readily available to the scan. The same
principles apply to all of the other MDAM techniques; they're
basically all just another flavor of skip scan (that do some kind of
clever preprocessing/transformation that enables reducing the scan to
a series of disjunctive accesses, and that could be implemented using
the new abstraction I'm calling skip arrays).

The paper more or less just applies one core idea, again and again.
It's surprising how far that one idea can take you. But it is still
just one core idea (don't overlook that point).

Range skip scan
---------------

To me, the most interesting MDAM technique is probably one that I
refer to as "range skip scan" in the patch. This is the technique that
the paper introduces first, in its "Intervening Range Predicates"
subsection. The best way of explaining it is through an example (you
could also just read the paper, which has an example of its own).

Imagine a table with just one index: a composite index on "(pdate,
customer_id)". Further suppose we have a query such as:

SELECT * FROM payments WHERE pdate BETWEEN '2024-01-01' AND
'2024-01-30' AND customer_id = 5; -- both index columns (pdate and
customer_id) appear in predicate

The patch effectively makes the nbtree code execute the index scan as
if the query had been written like this instead:

SELECT * FROM payments WHERE pdate = ANY ('2024-01-01', '2024-01-02',
..., '2024-01-30') AND customer_id = 5;

The use of a "range skip array" within nbtree allows the scan to skip
when that makes sense, locating the next date with customer_id = 5
each time (we might skip over many irrelevant leaf pages each time).
The scan must also *avoid* skipping when it *doesn't* make sense.

As always (since commit 5bf748b8 went in), whether and to what extent
we skip using array keys depends in large part on the physical
characteristics of the index at runtime. If the tuples that we need to
return are all clustered closely together, across only a handful of
leaf pages, then we shouldn't be skipping at all. When skipping makes
sense, we should skip constantly.

I'll discuss the trade-offs in this area a little more below, under "Design".

Using multiple MDAM techniques within the same index scan (includes benchmark)
------------------------------------------------------------------------------

I recreated the data in the MDAM paper's "sales" table by making
inferences from the paper. It's very roughly the same data set as the
paper (close enough to get the general idea across). The table size is
about 51GB, and the index is about 25GB (most of the attributes from
the table are used as index columns). There is nothing special about
this data set -- I just thought it would be cool to "recreate" the
queries from the paper, as best I could. Thought that this approach
might make my points about the design easier to follow.

The index we'll be using for this can be created via: "create index
mdam_idx on sales_mdam_paper(dept, sdate, item_class, store)". Again,
this is per the paper. It's also the order that the columns appear in
every WHERE clause in every query from the paper.

(That said, the particular column order from the index definition
mostly doesn't matter. Every index column is a low cardinality column,
so unless the order used completely obviates the need to skip a column
that would otherwise need to be skipped, such as "dept", the effect on
query execution time from varying column order is in the noise.
Obviously that's very much not how users are used to thinking about
composite indexes.)

The MDAM paper has numerous example queries, each of which builds on
the last, adding one more complication each time -- each of which is
addressed by another MDAM technique. The query I'll focus on here is
an example query that's towards the end of the paper, and so combines
multiple techniques together -- it's the query that appears in the "IN
Lists" subsection:

select
dept,
sdate,
item_class,
store,
sum(total_sales)
from
sales_mdam_paper
where
-- omitted: leading "dept" column from composite index
sdate between '1995-06-01' and '1995-06-30'
and item_class in (20, 35, 50)
and store in (200, 250)
group by dept, sdate, item_class, store
order by dept, sdate, item_class, store;

On HEAD, when we run this query we either get a sequential scan (which
is very slow) or a full index scan (which is almost as slow). Whereas
with the patch, nbtree will execute the query as a succession of a few
thousand very selective primitive index scans (which usually only scan
one leaf page, though some may scan two neighboring leaf pages).

Results: The full index scan on HEAD takes about 32 seconds. With the
patch, the query takes just under 52ms to execute. That works out to
be about 630x faster with the patch.

See the attached SQL file for full details. It provides all you'll
need to recreate this test result with the patch.

Nobody would put up with such an inefficient full index scan in the
first place, so the behavior on HEAD is not really a sensible baseline
-- 630x isn't very meaningful. I could have come up with a case that
showed an even larger improvement if I'd felt like it, but that
wouldn't have proven anything.

The important point is that the patch makes a huge composite index
like the one I've built for this actually make sense, for the first
time. So we're not so much making something faster as enabling a whole
new approach to indexing -- particularly for data warehousing use
cases. The way that Postgres DBAs choose which indexes they'll need to
create is likely to be significantly changed by this optimization.

I'll break down how this is possible. This query makes use of 3
separate MDAM techniques:

1. A "simple" skip scan (on "dept").

2. A "range" skip scan (on "sdate").

3. The pair of IN() lists/SAOPs on item_class and on store. (Nothing
new here, except that nbtree needs these regular SAOP arrays to roll
over the higher-order skip arrays to trigger moving on to the next
dept/date.)

Internally, we're just doing a series of several thousand distinct
non-overlapping accesses, in index key space order (so as to preserve
the appearance of one continuous index scan). These accesses starts
out like this:

dept=INT_MIN, date='1995-06-01', item_class=20, store=200
(Here _bt_advance_array_keys discovers that the actual lowest dept
is 1, not INT_MIN)
dept=1, date='1995-06-01', item_class=20, store=200
dept=1, date='1995-06-01', item_class=20, store=250
dept=1, date='1995-06-01', item_class=35, store=200
dept=1, date='1995-06-01', item_class=35, store=250
...

(Side note: as I mentioned, each of the two "store" values usually
appear together on the same leaf page in practice. Arguably I should
have shown 2 lines/accesses here (for "dept=1"), rather than showing
4. The 4 "dept=1" lines shown required only 2 primitive index
scans/index descents/leaf page reads. Disjunctive accesses don't
necessarily map 1:1 with primitive/physical index scans.)

About another ten thousand similar accesses occur (omitted for
brevity). Execution of the scan within nbtree finally ends with these
primitive index scans/accesses:
...
dept=100, date='1995-06-30', item_class=50, store=250
dept=101, date='1995-06-01', item_class=20, store=200
STOP

There is no "dept=101" entry in the index (the highest department in
the index happens to be 100). The index scan therefore terminates at
this point, having run out of leaf pages to scan (we've reached the
rightmost point of the rightmost leaf page, as the scan attempts to
locate non-existent dept=101 tuples).

Design
======

Since index scans with skip arrays work just like index scans with
regular arrays (as of Postgres 17), naturally, there are no special
restrictions. Associated optimizer index paths have path keys, and so
could (just for example) appear in a merge join, or feed into a group
aggregate, while avoiding a sort node. Index scans that skip could
also feed into a relocatable cursor.

As I mentioned already, the patch adds a skipping mechanism that is
purely an additive thing. I think that this will turn out to be an
important enabler of using the optimizations, even when there's much
uncertainty about how much they'll actually help at runtime.

Optimizer
---------

We make a broad assumption that skipping is always to our advantage
during nbtree preprocessing -- preprocessing generates as many skip
arrays as could possibly make sense based on static rules (rules that
don't apply any kind of information about data distribution). Of
course, skipping isn't necessarily the best approach in all cases, but
that's okay. We only actually skip when physical index characteristics
show that it makes sense. The real decisions about skipping are all
made dynamically.

That approach seems far more practicable than preempting the problem
during planning or during nbtree preprocessing. It seems like it'd be
very hard to model the costs statistically. We need revisions to
btcostestimate, of course, but the less we can rely on btcostestimate
the better. As I said, there are no new index paths generated by the
optimizer for any of this.

What do you call an index scan where 90% of all index tuples are 1 of
only 3 distinct values, while the remaining 10% of index tuples are
all perfectly unique in respect of a leading column? Clearly the best
strategy when skipping using the leading column to "use skip scan for
90% of the index, and use a conventional range scan for the remaining
10%". Skipping generally makes sense, but we legitimately need to vary
our strategy *during the same index scan*. It makes no sense to think
of skip scan as a discrete sort of index scan.

I have yet to prove that always having the option of skipping (even
when it's very unlikely to help) really does "come for free" -- for
now I'm just asserting that that's possible. I'll need proof. I expect
to hear some principled skepticism on this point. It's probably not
quite there in this v1 of the patch -- there'll be some regressions (I
haven't looked very carefully just yet). However, we seem to already
be quite close to avoiding regressions from excessive/useless
skipping.

Extensible infrastructure/support functions
-------------------------------------------

Currently, the patch only supports skip scan for a subset of all
opclasses -- those that have the required support function #6, or
"skip support" function. This provides the opclass with (among other
things) a way to increment the current skip array value (or to
decrement it, in the case of backward scans). In practice we only have
this for a handful of discrete integer (and integer-ish) types. Note
that the patch currently cannot skip for an index column that happens
to be text. Note that even this v1 supports skip scans that use
unsupported types, provided that the input opclass of the specific
columns we'll need to skip has support.

The patch should be able to support every type/opclass as a target for
skipping, regardless of whether an opclass support function happened
to be available. That could work by teaching the nbtree code to have
explicit probes for the next skip array value in the index, only then
combining that new value with the qual from the input scan keys/query.
I've put that off for now because it seems less important -- it
doesn't really affect anything I've said about the core design, which
is what I'm focussing on for now.

It makes sense to use increment/decrement whenever feasible, even
though it isn't strictly necessary (or won't be, once the patch has
the required explicit probe support). The only reason to not apply
increment/decrement opclass skip support (that I can see) is because
it just isn't practical (this is generally the case for continuous
types). While it's slightly onerous to have to invent all this new
opclass infrastructure, it definitely makes sense.

There is a performance advantage to having skip arrays that can
increment through each distinct possible indexable value (this
increment/decrement stuff comes from the MDAM paper). The MDAM
techniques inherently work best when "skipping" columns of discrete
types like integer and date, which is why the paper has examples that
all look like that. If you look at my example query and its individual
accesses, you'll realize why this is so.

Thoughts?

[1]: https://vldb.org/conf/1995/P710.PDF -- Peter Geoghegan
--
Peter Geoghegan

Attachments:

mdam_paper_in_query.sqlapplication/octet-stream; name=mdam_paper_in_query.sqlDownload
v1-0001-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v1-0001-Add-skip-scan-to-nbtree.patchDownload
From 932bb757f5e8b5ee23c842438da93d39d8b4a1a7 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v1] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on an index (a, b) for queries with a predicate such as "WHERE b = 5".
This is useful in cases where the total number of distinct values in the
column 'a' is reasonably small (think hundreds, possibly thousands).

In effect, a skip scan treats the composite index on (a, b) as if it was
a series of disjunct subindexes -- one subindex per distinct 'a' value.
We exhaustively "search every subindex" using a qual that behaves like
"WHERE a = ANY(<every possible 'a' value>) AND b = 5".
---
 src/include/access/nbtree.h                 |   16 +-
 src/include/catalog/pg_amproc.dat           |   16 +
 src/include/catalog/pg_proc.dat             |   24 +
 src/include/utils/skipsupport.h             |  140 ++
 src/backend/access/nbtree/nbtcompare.c      |  199 +++
 src/backend/access/nbtree/nbtree.c          |    5 +-
 src/backend/access/nbtree/nbtutils.c        | 1378 +++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c     |    4 +
 src/backend/commands/opclasscmds.c          |   25 +
 src/backend/utils/adt/Makefile              |    1 +
 src/backend/utils/adt/date.c                |   34 +
 src/backend/utils/adt/meson.build           |    1 +
 src/backend/utils/adt/selfuncs.c            |   30 +-
 src/backend/utils/adt/skipsupport.c         |   54 +
 src/backend/utils/adt/uuid.c                |   65 +
 src/backend/utils/misc/guc_tables.c         |   12 +
 doc/src/sgml/btree.sgml                     |   13 +
 doc/src/sgml/xindex.sgml                    |   16 +-
 src/test/regress/expected/alter_generic.out |    6 +-
 src/test/regress/expected/psql.out          |    3 +-
 src/test/regress/sql/alter_generic.sql      |    2 +-
 src/tools/pgindent/typedefs.list            |    3 +
 22 files changed, 1882 insertions(+), 165 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 749304334..81e99fcc1 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1032,9 +1034,15 @@ typedef BTScanPosData *BTScanPos;
 typedef struct BTArrayKeyInfo
 {
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* State used by standard arrays that store elements in memory */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* State used by skip arrays, which generate elements procedurally */
+	SkipSupportData sksup;		/* opclass skip scan support */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1123,6 +1131,7 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* SK_SEARCHARRAY skip scan key */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1159,6 +1168,9 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameter (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index f639c3a6a..2a8f6f3f1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -122,6 +128,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +149,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +170,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +205,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +275,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6a5476d3c..888a4893c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2214,6 +2229,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4368,6 +4386,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -9175,6 +9196,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..a71a624d0
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,140 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scans.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * This happens at the point where the scan determines that another primitive
+ * index scan is required.  The next value is used (in combination with at
+ * least one additional lower-order non-skip key, taken from the SQL query) to
+ * relocate the scan, skipping over many irrelevant leaf pages in the process.
+ *
+ * There are many data types/opclasses where implementing a skip support
+ * scheme is inherently impossible (or at least impractical).  Obviously, it
+ * would be wrong if the "next" value generated by an opclass was actually
+ * after the true next value (any index tuples with the true next value would
+ * be overlooked by the index scan).  This partly explains why opclasses are
+ * under no obligation to implement skip support: a continuous type may have
+ * no way of generating a useful next value.
+ *
+ * Skip scan generally works best with discrete types such as integer, date,
+ * and boolean: types where we expect indexes to contain large groups of
+ * contiguous values (in respect of the leading/skipped index attribute).
+ * When gaps/discontinuities are naturally rare (e.g., a leading identity
+ * column in a composite index, a date column preceding a product_id column),
+ * then it makes sense for the skip scan to optimistically assume that the
+ * next distinct indexable value will find directly matching index tuples.
+ * The B-Tree code can fall back on explicit next-key probes for any opclass
+ * that doesn't include a skip support function, but it's best to provide skip
+ * support whenever possible.  The B-Tree code assumes that it's always better
+ * to use the opclass skip support routine where available.
+ *
+ * When a skip scan "bets" that the next indexable value will find an exact
+ * match, there is significant upside, without any accompanying downside.
+ * When this optimistic strategy works out, the scan avoids the cost of an
+ * explicit probe (used in the no-skip-support case to determine the true next
+ * value in the index's skip attribute).  When the strategy doesn't work out,
+ * then the scan is no worse off than it would have been without skip support.
+ * The explicit next-key probes used by B-Tree skip scan's fallback path are
+ * very similar to "failed" optimistic searches for the next indexable value
+ * (the next value according to the opclass skip support routine).
+ *
+ * (FIXME Actually, nbtree does no such thing right now, which is considered a
+ * blocker to commit.)
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called.
+ * If an opclass can only set some of the fields, then it cannot safely
+ * provide a skip support routine (and so must rely on the fallback strategy
+ * used by continuous types, such as numeric).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming standard ascending
+	 * order).  This helps the B-Tree code with finding its initial position
+	 * at the leaf level (during the skip scan's first primitive index scan).
+	 * In other words, it gives the B-Tree code a useful value to start from,
+	 * before any data has been read from the index.
+	 *
+	 * low_elem and high_elem can also be used to prove that a qual is
+	 * unsatisfiable in certain cross-type scenarios.
+	 *
+	 * low_elem and high_elem are also used by skip scans to determine when
+	 * they've reached the final possible value (in the current direction).
+	 * It's typical for the scan to run out of leaf pages before it runs out
+	 * of unscanned indexable values, but it's still useful for the scan to
+	 * have a way to recognize when it has reached the last possible value
+	 * (this saves us a useless probe that just lands on the final leaf page).
+	 *
+	 * Note: the logic for determining that the scan has reached the final
+	 * possible value naturally belongs in the B-Tree code.  The final value
+	 * isn't necessarily the original high_elem/low_elem set by the opclass.
+	 * In particular, it'll be a lower/higher value when B-Tree preprocessing
+	 * determines that the true range of possible values should be restricted,
+	 * due to the presence of an inequality applied to the index's skipped
+	 * attribute.  These are range skip scans.
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (in the case of pass-by-reference
+	 * types).  It's not okay for these functions to leak any memory.
+	 *
+	 * Both decrement and increment callbacks are guaranteed to never be
+	 * called with a NULL "existing" arg.  (In general it is the B-Tree code's
+	 * job to worry about NULLs, and about whether indexed values are stored
+	 * in ASC order or DESC order.)
+	 *
+	 * The decrement callback is guaranteed to only be called with an
+	 * "existing" value that's strictly > the low_elem set by the opclass.
+	 * Similarly, the increment callback is guaranteed to only be called with
+	 * an "existing" value that's strictly < the high_elem set by the opclass.
+	 * Consequently, opclasses don't have to deal with "overflow" themselves
+	 * (though asserting that the B-Tree code got it right is a good idea).
+	 *
+	 * It's quite possible (and very common) for the B-Tree skip scan caller's
+	 * "existing" datum to just be a straight copy of a value that it copied
+	 * from the index.  Operator classes must be liberal in accepting every
+	 * possible representational variation within the underlying data type.
+	 * Opclasses don't have to preserve whatever semantically insignificant
+	 * information the data type might be carrying around, though.
+	 *
+	 * Note: < and > are defined by the opclass's ORDER proc in the usual way.
+	 */
+	Datum		(*decrement) (Relation rel, Datum existing);
+	Datum		(*increment) (Relation rel, Datum existing);
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..c451c7b02 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,39 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	Assert(bexisting == true);
+
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	Assert(bexisting == false);
+
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +139,39 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	Assert(iexisting > PG_INT16_MIN);
+
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	Assert(iexisting < PG_INT16_MAX);
+
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +195,39 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	Assert(iexisting > PG_INT32_MIN);
+
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	Assert(iexisting < PG_INT32_MAX);
+
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +271,39 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	Assert(iexisting > PG_INT64_MIN);
+
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	Assert(iexisting < PG_INT64_MAX);
+
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +425,39 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	Assert(oexisting > InvalidOid);
+
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	Assert(oexisting < OID_MAX);
+
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +491,36 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing)
+{
+	char		cexisting = DatumGetChar(existing);
+
+	Assert(cexisting > SCHAR_MIN);
+
+	return CharGetDatum(cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing)
+{
+	char		cexisting = DatumGetChar(existing);
+
+	Assert(cexisting < SCHAR_MAX);
+
+	return CharGetDatum(cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+	sksup->low_elem = CharGetDatum(SCHAR_MIN);
+	sksup->high_elem = CharGetDatum(SCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 686a3206f..5cc520fa4 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -324,10 +324,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	so = (BTScanOpaque) palloc(sizeof(BTScanOpaqueData));
 	BTScanPosInvalidate(so->currPos);
 	BTScanPosInvalidate(so->markPos);
-	if (scan->numberOfKeys > 0)
-		so->keyData = (ScanKey) palloc(scan->numberOfKeys * sizeof(ScanKeyData));
-	else
-		so->keyData = NULL;
+	so->keyData = NULL;
 
 	so->needPrimScan = false;
 	so->scanBehind = false;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index d6de2072d..295179392 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -28,10 +28,44 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/skipsupport.h"
+
+/*
+ * GUC parameter (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.
+ *
+ * For example, setting skipscan_prefix_cols=1 before an index scan with qual
+ * "WHERE b = 1 AND c > 42" will make us generate a skip scan key on the
+ * column 'a' (which is attnum 1) only, preventing us from adding one for the
+ * column 'c' (and so 'c' will still have an inequality scan key, required in
+ * only one direction -- 'c' won't be output as a "range" skip key/array).
+ *
+ * The same scan keys will be output when skipscan_prefix_cols=2, given the
+ * same query/qual, since we naturally get a required equality scan key on 'b'
+ * from the input scan keys (provided we at least manage to add a skip scan
+ * key on 'a' that "anchors its required-ness" to the 'b' scan key.)
+ *
+ * When skipscan_prefix_cols is set to the number of key columns in the index,
+ * we're as aggressive as possible about adding skip scan arrays/scan keys.
+ * This is the current default behavior, and the behavior we're targeting for
+ * the committed patch (if there are slowdowns from being maximally aggressive
+ * here then the likely solution is to make _bt_advance_array_keys adaptive,
+ * rather than trying to predict what will work during preprocessing).
+ */
+int			skipscan_prefix_cols;
 
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support */
+	Oid			eq_op;			/* InvalidOid means don't skip */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -62,18 +96,49 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
-static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan);
+static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts);
+static bool _bt_skip_support(Relation rel, int add_skip_attno,
+							 BTSkipPreproc *skipatts);
+static inline Datum _bt_apply_decrement(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array);
+static inline Datum _bt_apply_increment(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
-										   Datum arrdatum, ScanKey cur);
+										   Datum arrdatum, bool arrnull,
+										   ScanKey cur);
+static void _bt_apply_compare_array(ScanKey arraysk, ScanKey skey,
+									FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_apply_compare_skiparray(IndexScanDesc scan, ScanKey arraysk,
+										ScanKey skey, FmgrInfo *orderproc,
+										FmgrInfo *orderprocp,
+										BTArrayKeyInfo *array, bool *qual_ok);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
+static bool _bt_advance_skip_array_key_increment(Relation rel, ScanDirection dir,
+												 BTArrayKeyInfo *array, ScanKey skey,
+												 FmgrInfo *orderproc);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 										 IndexTuple tuple, TupleDesc tupdesc, int tupnatts,
@@ -251,9 +316,6 @@ _bt_freestack(BTStack stack)
  * It is convenient for _bt_preprocess_keys caller to have to deal with no
  * more than one equality strategy array scan key per index attribute.  We'll
  * always be able to set things up that way when complete opfamilies are used.
- * Eliminated array scan keys can be recognized as those that have had their
- * sk_strategy field set to InvalidStrategy here by us.  Caller should avoid
- * including these in the scan's so->keyData[] output array.
  *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
@@ -261,18 +323,36 @@ _bt_freestack(BTStack stack)
  * preprocessing steps are complete.  This will convert the scan key offset
  * references into references to the scan's so->keyData[] output scan keys.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by later preprocessing on output.
+ * _bt_decide_skipatts decides which attributes receive skip arrays.
+ *
+ * Caller must pass *numberOfKeys to give us a way to change the number of
+ * input scan keys (our output is caller's input).  The returned array can be
+ * smaller than scan->keyData[] when we eliminated a redundant array scan key
+ * (redundant with some other array scan key, for the same attribute).  It can
+ * also be larger when we added a skip array/skip scan key.  Caller uses this
+ * to allocate so->keyData[] for the current btrescan.
+ *
  * Note: the reason we need to return a temp scan key array, rather than just
  * scribbling on scan->keyData, is that callers are permitted to call btrescan
  * without supplying a new set of scankey data.
  */
 static ScanKey
-_bt_preprocess_array_keys(IndexScanDesc scan)
+_bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
+	int			numArrayKeyData = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
-	int			numArrayKeys;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
+	int			numArrayKeys,
+				numSkipArrayKeys,
+				output_ikey = 0;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -280,11 +360,14 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
+	Assert(scan->numberOfKeys);
 
-	/* Quick check to see if there are any array keys */
+	/*
+	 * Quick check to see if there are any array keys, or any missing keys we
+	 * can generate a "skip scan" array key for ourselves
+	 */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
 		cur = &scan->keyData[i];
 		if (cur->sk_flags & SK_SEARCHARRAY)
@@ -300,6 +383,16 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		}
 	}
 
+	/* Consider generating skip arrays, and associated equality scan keys */
+	numSkipArrayKeys = _bt_decide_skipatts(scan, skipatts);
+	if (numSkipArrayKeys)
+	{
+		/* At least one skip array scan key must be added to arrayKeyData[] */
+		numArrayKeys += numSkipArrayKeys;
+		/* output scan key buffer allocation needs space for skip scan keys */
+		numArrayKeyData += numSkipArrayKeys;
+	}
+
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
@@ -317,19 +410,23 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
-	/* Create modifiable copy of scan->keyData in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
-	memcpy(arrayKeyData, scan->keyData, numberOfKeys * sizeof(ScanKeyData));
+	/* Create output scan keys in the workspace context */
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/*
+	 * Process each array key, and generate skip arrays as needed.  Also copy
+	 * every scan->keyData[] input scan key (whether it's an array or not)
+	 * into the arrayKeyData array we'll return to our caller (barring any
+	 * array scan keys that we could eliminate early through array merging).
+	 */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -345,14 +442,77 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		int			num_nonnulls;
 		int			j;
 
-		cur = &arrayKeyData[i];
-		if (!(cur->sk_flags & SK_SEARCHARRAY))
-			continue;
+		/* Create a skip array and scan key where indicated by skipatts */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(skipatts[attno_skip - 1].eq_op))
+			{
+				/* won't skip using this attribute */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[output_ikey];
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how the
+			 * consed-up "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[output_ikey], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			output_ikey++;		/* keep this scan key/array */
+		}
 
 		/*
-		 * First, deconstruct the array into elements.  Anything allocated
-		 * here (including a possibly detoasted array value) is in the
-		 * workspace context.
+		 * Copy input scan key into temp arrayKeyData scan key array.  (From
+		 * here on, cur points at our copy of the input scan key.)
+		 */
+		cur = &arrayKeyData[output_ikey];
+		*cur = scan->keyData[input_ikey];
+
+		if (!(cur->sk_flags & SK_SEARCHARRAY))
+		{
+			output_ikey++;		/* keep this scan key */
+			continue;
+		}
+
+		/*
+		 * Deconstruct the array into elements
 		 */
 		arrayval = DatumGetArrayTypeP(cur->sk_argument);
 		/* We could cache this data, but not clear it's worth it */
@@ -406,6 +566,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
+				output_ikey++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -416,6 +577,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
+				output_ikey++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -432,7 +594,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[i], &sortprocp);
+							&so->orderProcs[output_ikey], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -476,11 +638,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					break;
 				}
 
-				/*
-				 * Indicate to _bt_preprocess_keys caller that it must ignore
-				 * this scan key
-				 */
-				cur->sk_strategy = InvalidStrategy;
+				/* Throw away this array (by not incrementing output_ikey) */
 				continue;
 			}
 
@@ -511,12 +669,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = i;
+		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
 		numArrayKeys++;
+		output_ikey++;			/* keep this scan key/array */
 	}
 
+	/* Set final number of arrayKeyData[] keys, array keys */
+	*numberOfKeys = output_ikey;
 	so->numArrayKeys = numArrayKeys;
 
 	MemoryContextSwitchTo(oldContext);
@@ -624,7 +785,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -685,6 +847,245 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_decide_skipatts() -- set index attributes requiring skip arrays
+ *
+ * _bt_preprocess_array_keys helper function.  Determines which attributes
+ * will require skip arrays/scan keys.  Also sets up skip support function for
+ * each of these attributes.
+ *
+ * This sets up "skip scan".  Adding skip arrays (and associated scan keys)
+ * allows _bt_preprocess_keys to mark lower-order scan keys (copied from the
+ * original scan->keyData[] array in the conventional way) as required.  The
+ * overall effect is to enable skipping over irrelevant sections of the index.
+ *
+ * Return value is the total number of scan keys to add as "input" scan keys
+ * for further processing within _bt_preprocess_keys.
+ */
+static int
+_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts)
+{
+	Relation	rel = scan->indexRelation;
+	ScanKey		inputsk;
+	AttrNumber	attno_inputsk = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numSkipArrayKeys = 0,
+				prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * XXX Don't support system catalogs for now.  Calls to routines like
+	 * get_opfamily_member() are prone to infinite recursion, which we'll need
+	 * to find workaround for (hard-coded lookups?).
+	 */
+	if (IsCatalogRelation(rel))
+		return 0;
+
+	/*
+	 * FIXME Also don't support parallel scans for now.  Must add logic to
+	 * places like _bt_parallel_primscan_schedule so that we account for skip
+	 * arrays when parallel workers serialize their array scan state.
+	 */
+	if (scan->parallel_scan)
+		return 0;
+
+	inputsk = &scan->keyData[0];
+	for (int i = 0;; inputsk++, i++)
+	{
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inputsk
+		 */
+		while (attno_skip < attno_inputsk)
+		{
+			if (!_bt_skip_support(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Opclass lacks a suitable skip support routine.
+				 *
+				 * Return prev_numSkipArrayKeys, so as to avoid including any
+				 * "backfilled" arrays that were supposed to form a contiguous
+				 * group with a skip array on this attribute.  There is no
+				 * benefit to adding backfill skip arrays unless we can do so
+				 * for all attributes (all attributes up to and including the
+				 * one immediately before attno_inputsk).
+				 */
+				return prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			numSkipArrayKeys++;
+			attno_skip++;
+		}
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i >= scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 * (either in part or in whole)
+		 */
+		if (attno_inputsk > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inputsk (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inputsk < inputsk->sk_attno)
+		{
+			prev_numSkipArrayKeys = numSkipArrayKeys;
+
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (!attno_has_equal)
+			{
+				/* Only saw inequalities for the prior attribute */
+				if (_bt_skip_support(rel, attno_skip,
+									 &skipatts[attno_skip - 1]))
+				{
+					/* add a range skip array for this attribute */
+					numSkipArrayKeys++;
+				}
+				else
+					break;
+			}
+			else
+			{
+				/*
+				 * Saw an equality for the prior attribute, so it doesn't need
+				 * a skip array (not even a range skip array).  We'll be able
+				 * to add later skip arrays, too (doesn't matter if the prior
+				 * attribute uses an input opclass without skip support).
+				 */
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inputsk = inputsk->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this scan key's attribute has any equality strategy scan
+		 * keys.
+		 *
+		 * Treat IS NULL scan keys as using equal strategy (they'll be marked
+		 * as using it later on, by _bt_fix_scankey_strategy).
+		 */
+		if (inputsk->sk_strategy == BTEqualStrategyNumber ||
+			(inputsk->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.
+		 *
+		 * We do still backfill skip attributes before the RowCompare, so that
+		 * it can be marked required.  This is similar to what happens when a
+		 * conventional inequality uses an opclass that lacks skip support.
+		 */
+		if (inputsk->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	return numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skip_support() -- set up skip support function in *skipatts
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's skip support routine for caller.  Otherwise returns false.
+ */
+static bool
+_bt_skip_support(Relation rel, int add_skip_attno, BTSkipPreproc *skipatts)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator */
+	skipatts->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										  BTEqualStrategyNumber);
+
+	/*
+	 * We don't expect input opclasses lacking even an equality operator, but
+	 * it's possible.  Deal with it gracefully.
+	 */
+	if (!OidIsValid(skipatts->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	return PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse,
+										 &skipatts->sksup);
+}
+
+/*
+ * _bt_apply_decrement() -- Get a decremented copy of skey's arg
+ *
+ * Note: this wrapper function calls the opclass increment function when the
+ * index stores values in descending order.  We're "logically decrementing" to
+ * the previous value in the key space regardless.
+ */
+static inline Datum
+_bt_apply_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	if (!(skey->sk_flags & SK_BT_DESC))
+		return array->sksup.decrement(rel, skey->sk_argument);
+	else
+		return array->sksup.increment(rel, skey->sk_argument);
+}
+
+/*
+ * _bt_apply_increment() -- Get an incremented copy of skey's arg
+ *
+ * Note: this wrapper function calls the opclass decrement function when the
+ * index stores values in descending order.  We're "logically incrementing" to
+ * the next value in the key space regardless.
+ */
+static inline Datum
+_bt_apply_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	if (!(skey->sk_flags & SK_BT_DESC))
+		return array->sksup.increment(rel, skey->sk_argument);
+	else
+		return array->sksup.decrement(rel, skey->sk_argument);
+}
+
 /*
  * _bt_setup_array_cmp() -- Set up array comparison functions
  *
@@ -979,15 +1380,10 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
@@ -1000,8 +1396,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.  We have to be sure
+	 * that _bt_compare_array_skey/_bt_binsrch_array_skey use the right proc.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1032,11 +1428,46 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	/*
+	 * We have all we need to determine redundancy/contradictoriness.
+	 *
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+		_bt_apply_compare_array(arraysk, skey,
+								orderprocp, array, qual_ok);
+	else
+		_bt_apply_compare_skiparray(scan, arraysk, skey,
+									orderproc, orderprocp,
+									array, qual_ok);
+
+	return true;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when it
+ * is redundant with (or contradicted by) a non-array scalar scan key.
+ *
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ */
+static void
+_bt_apply_compare_array(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1088,8 +1519,152 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
 
-	return true;
+/*
+ * Finish off preprocessing of skip array scan key when it is redundant with
+ * (or contradicted by) a non-array scalar scan key.
+ *
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Arrays used to skip (skip scan/missing key attribute predicates) work by
+ * procedurally generating their elements on the fly.  We must still
+ * "eliminate contradictory elements", but it works a little differently: we
+ * narrow the range of the skip array, such that the array will never
+ * generated contradicted-by-skey elements.
+ *
+ * FIXME Our behavior in scenarios with cross-type operators (range skip scan
+ * cases) is buggy.  We're naively copying datums of a different type from
+ * scalar inequality scan keys into the array's low_value and high_value
+ * fields.  In practice this tends to not visibly break (in practice types
+ * that appear within the same operator family tend to have compatible datum
+ * representations, at least on systems with little-endian byte order).  Put
+ * off dealing with the problem until a later revision of the patch.
+ *
+ * It seems likely that the best way to fix this problem will involve keeping
+ * around the original operator in the BTArrayKeyInfo array struct whenever
+ * we're passed a "redundant" cross-type inequality operator (an approach
+ * involving casts/coercions might be tempting, but seems much too fragile).
+ * We only need to use not-column-input-opclass-type operators for the first
+ * and/or last array elements from the skip array under this scheme; we'll
+ * still mostly be dealing with opcintype-typed datums, copied from the index
+ * (as well as incrementing/decrementing copies of those index tuple datums).
+ * Importantly, this scheme should work just as well with an opfamily that
+ * doesn't even have an orderprocp cross-type ORDER operator to pass us here
+ * (we might even have to keep more than one same-strategy inequality, since
+ * in general _bt_preprocess_keys might not be able to prove which inequality
+ * is redundant).
+ */
+static void
+_bt_apply_compare_skiparray(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							FmgrInfo *orderproc, FmgrInfo *orderprocp,
+							BTArrayKeyInfo *array, bool *qual_ok)
+{
+	Relation	rel = scan->indexRelation;
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	Form_pg_attribute attr = TupleDescAttr(RelationGetDescr(rel),
+										   skey->sk_attno - 1);
+	MemoryContext oldContext;
+	int			cmpresult;
+
+	/*
+	 * We don't expect to have to deal with NULLs in non-array/non-skip scan
+	 * key.  We expect _bt_preprocess_array_keys to avoid generating a skip
+	 * array for an index attribute with an IS NULL input scan key. It will
+	 * still do so in the presence of IS NOT NULL input scan keys, but
+	 * _bt_compare_scankey_args is expected to handle those for us.
+	 */
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(arraysk->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & SK_ISNULL));
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Scalar scan key must be a B-Tree operator, which must always be strict.
+	 * Array shouldn't generate a NULL "array element"/an IS NULL qual.  This
+	 * isn't just an optimization; it's strictly necessary for correctness.
+	 */
+	array->null_elem = false;
+
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+
+			/*
+			 * detect if scan key argument will be < low_value once
+			 * decremented
+			 */
+			cmpresult = _bt_compare_array_skey(orderprocp,
+											   skey->sk_argument, false,
+											   array->sksup.low_elem, false,
+											   arraysk);
+			if (cmpresult <= 0)
+			{
+				/* decrementing would make qual unsatisfiable, so don't try */
+				*qual_ok = false;
+				return;
+			}
+
+			/* decremented scan key value becomes skip array's new high_value */
+			oldContext = MemoryContextSwitchTo(so->arrayContext);
+			array->sksup.high_elem = _bt_apply_decrement(rel, skey, array);
+			MemoryContextSwitchTo(oldContext);
+			break;
+		case BTLessEqualStrategyNumber:
+			oldContext = MemoryContextSwitchTo(so->arrayContext);
+			array->sksup.high_elem = datumCopy(skey->sk_argument,
+											   attr->attbyval, attr->attlen);
+			MemoryContextSwitchTo(oldContext);
+			break;
+		case BTEqualStrategyNumber:
+			/* _bt_preprocess_array_keys should have avoided this */
+			elog(ERROR, "equality strategy scan key conflicts with skip key for attribute %d on index \"%s\"",
+				 skey->sk_attno, RelationGetRelationName(rel));
+			break;
+		case BTGreaterEqualStrategyNumber:
+			oldContext = MemoryContextSwitchTo(so->arrayContext);
+			array->sksup.low_elem = datumCopy(skey->sk_argument,
+											  attr->attbyval, attr->attlen);
+			MemoryContextSwitchTo(oldContext);
+			break;
+		case BTGreaterStrategyNumber:
+
+			/*
+			 * detect if scan key argument will be > high_value once
+			 * incremented
+			 */
+			cmpresult = _bt_compare_array_skey(orderprocp,
+											   skey->sk_argument, false,
+											   array->sksup.high_elem, false,
+											   arraysk);
+			if (cmpresult >= 0)
+			{
+				/* incrementing would make qual unsatisfiable, so don't try */
+				*qual_ok = false;
+				return;
+			}
+
+			/* incremented scan key value becomes skip array's new low_value */
+			oldContext = MemoryContextSwitchTo(so->arrayContext);
+			array->sksup.low_elem = _bt_apply_increment(rel, skey, array);
+			MemoryContextSwitchTo(oldContext);
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	/*
+	 * Is the qual contradictory, or is it merely "redundant" with consed-up
+	 * skip array?
+	 */
+	cmpresult = _bt_compare_array_skey(orderproc,	/* don't use orderprocp */
+									   array->sksup.low_elem, false,
+									   array->sksup.high_elem, false,
+									   arraysk);
+	*qual_ok = (cmpresult <= 0);
 }
 
 /*
@@ -1130,7 +1705,8 @@ _bt_compare_array_elements(const void *a, const void *b, void *arg)
 static inline int32
 _bt_compare_array_skey(FmgrInfo *orderproc,
 					   Datum tupdatum, bool tupnull,
-					   Datum arrdatum, ScanKey cur)
+					   Datum arrdatum, bool arrnull,
+					   ScanKey cur)
 {
 	int32		result = 0;
 
@@ -1138,14 +1714,14 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 
 	if (tupnull)				/* NULL tupdatum */
 	{
-		if (cur->sk_flags & SK_ISNULL)
+		if (arrnull)
 			result = 0;			/* NULL "=" NULL */
 		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = -1;		/* NULL "<" NOT_NULL */
 		else
 			result = 1;			/* NULL ">" NOT_NULL */
 	}
-	else if (cur->sk_flags & SK_ISNULL) /* NOT_NULL tupdatum, NULL arrdatum */
+	else if (arrnull)			/* NOT_NULL tupdatum, NULL arrdatum */
 	{
 		if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = 1;			/* NOT_NULL ">" NULL */
@@ -1211,6 +1787,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1246,7 +1824,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[low_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result <= 0)
 				{
@@ -1274,7 +1852,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[high_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result >= 0)
 				{
@@ -1301,7 +1879,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 		arrdatum = array->elem_values[mid_elem];
 
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										arrdatum, cur);
+										arrdatum, false, cur);
 
 		if (result == 0)
 		{
@@ -1326,13 +1904,123 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	 */
 	if (low_elem != mid_elem)
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										array->elem_values[low_elem], cur);
+										array->elem_values[low_elem], false,
+										cur);
 
 	*set_elem_result = result;
 
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Skip scan arrays procedurally generate their elements on-demand.  They
+ * largely function in the same way as standard arrays.  They can be rolled
+ * over by standard arrays (standard array can also roll over skip arrays).
+ *
+ * This routine doesn't return an index into the array, because the array
+ * doesn't actually have any elements (it has low_value and high_value, which
+ * indicate the range of values that the array can generate).  Note that this
+ * may include a NULL value/an IS NULL qual (unlike with true arrays).
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this skip
+ * array's scan key.  We can apply this information to find the next matching
+ * array element in the current scan direction using fewer comparisons.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Datum		arrdatum;
+	bool		arrnull;
+
+	Assert(!ScanDirectionIsNoMovement(dir));
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Compare tupdatum against "first array element" in the current scan
+	 * direction first (and allow NULL to be treated as a possible element).
+	 *
+	 * Optimization: don't have to bother with this when passed a skip array
+	 * that is known to have triggered array advancement.
+	 */
+	if (!cur_elem_trig)
+	{
+		if (ScanDirectionIsForward(dir))
+		{
+			arrdatum = array->sksup.low_elem;
+			arrnull = array->null_elem && (cur->sk_flags & SK_BT_NULLS_FIRST);
+		}
+		else
+		{
+			arrdatum = array->sksup.high_elem;
+			arrnull = array->null_elem && !(cur->sk_flags & SK_BT_NULLS_FIRST);
+		}
+
+		*set_elem_result = _bt_compare_array_skey(orderproc,
+												  tupdatum, tupnull,
+												  arrdatum, arrnull,
+												  cur);
+
+		/*
+		 * Optimization: return early when >= lower bound happens to be an
+		 * exact match (or when <= upper bound is an exact match during a
+		 * backwards scan)
+		 */
+		if (*set_elem_result == 0)
+			return;
+
+		/* Is tupdatum "before the start" of our lowest "element"? */
+		if ((ScanDirectionIsForward(dir) && *set_elem_result < 0) ||
+			(ScanDirectionIsBackward(dir) && *set_elem_result > 0))
+			return;
+	}
+
+	/*
+	 * Now compare tupdatum to the "last array element" in the current scan
+	 * direction (and allow NULL to be treated as a possible element)
+	 */
+	if (ScanDirectionIsForward(dir))
+	{
+		arrdatum = array->sksup.high_elem;
+		arrnull = array->null_elem && !(cur->sk_flags & SK_BT_NULLS_FIRST);
+	}
+	else
+	{
+		arrdatum = array->sksup.low_elem;
+		arrnull = array->null_elem && (cur->sk_flags & SK_BT_NULLS_FIRST);
+	}
+
+	*set_elem_result = _bt_compare_array_skey(orderproc,
+											  tupdatum, tupnull,
+											  arrdatum, arrnull,
+											  cur);
+
+	/* Is tupdatum "after the end" of our highest "element"? */
+	if ((ScanDirectionIsForward(dir) && *set_elem_result > 0) ||
+		(ScanDirectionIsBackward(dir) && *set_elem_result < 0))
+		return;
+
+	/*
+	 * tupdatum must be within the range of the skip array.  Have our caller
+	 * treat tupdatum as one of the array's elements.
+	 */
+	*set_elem_result = 0;
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1342,29 +2030,256 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
 		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
 		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, curArrayKey,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = false;
 }
 
+/*
+ * _bt_scankey_decrement() -- decrement scan key's sk_argument
+ *
+ * Unsets scan key "IS NULL" flags when required, and handles memory
+ * management for pass-by-reference types.
+ */
+static void
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (skey->sk_flags & SK_ISNULL)
+		_bt_scankey_unset_isnull(rel, skey, array);
+	else
+	{
+		Datum		dec_sk_argument;
+		Form_pg_attribute attr;
+
+		/* Get a decremented copy of existing sk_argument */
+		dec_sk_argument = _bt_apply_decrement(rel, skey, array);
+
+		/* Free memory previously allocated for sk_argument if needed */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+
+		/* Set decremented copy of original sk_argument in scan key */
+		skey->sk_argument = dec_sk_argument;
+	}
+}
+
+/*
+ * _bt_scankey_increment() -- increment scan key's sk_argument
+ *
+ * Unsets scan key "IS NULL" flags when required, and handles memory
+ * management for pass-by-reference types.
+ */
+static void
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (skey->sk_flags & SK_ISNULL)
+		_bt_scankey_unset_isnull(rel, skey, array);
+	else
+	{
+		Datum		inc_sk_argument;
+		Form_pg_attribute attr;
+
+		/* Get an incremented copy of existing sk_argument */
+		inc_sk_argument = _bt_apply_increment(rel, skey, array);
+
+		/* Free memory previously allocated for sk_argument if needed */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+
+		/* Set incremented copy of original sk_argument in scan key */
+		skey->sk_argument = inc_sk_argument;
+	}
+}
+
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Set element to NULL (lowest/highest element) */
+		skey->sk_argument = (Datum) 0;
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else
+	{
+		/* Lowest array element isn't NULL */
+		if (low_not_high)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/*
+	 * Treat tupdatum/tupnull as a matching array element.
+	 *
+	 * We just copy tupdatum into the array's scan key (there is no
+	 * conventional array element for us to set, of course).
+	 */
+	if (tupnull)
+	{
+		/*
+		 * Unlike standard arrays, skip arrays sometimes need to locate NULLs.
+		 * We can treat them as just another value from the domain of indexed
+		 * values.
+		 */
+		Assert(array->null_elem);
+
+		skey->sk_argument = (Datum) 0;
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else
+	{
+		skey->sk_argument = datumCopy(tupdatum,
+									  attr->attbyval, attr->attlen);
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	}
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(array->null_elem);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- increment/decrement scan key to NULL
+ *
+ * Sets scan key to "IS NULL", and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & SK_ISNULL));
+	Assert(array->null_elem);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1380,6 +2295,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1391,10 +2307,24 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	{
 		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
 		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		FmgrInfo   *orderproc = &so->orderProcs[curArrayKey->scan_key];
 		int			cur_elem = curArrayKey->cur_elem;
 		int			num_elems = curArrayKey->num_elems;
 		bool		rolled = false;
 
+		/* Handle incrementing a skip array */
+		if (num_elems == -1)
+		{
+			/* Attempt to incrementally advance this skip scan array */
+			if (_bt_advance_skip_array_key_increment(rel, dir, curArrayKey,
+													 skey, orderproc))
+				return true;
+
+			/* Array rolled over.  Need to advance next array key, if any. */
+			continue;
+		}
+
+		/* Handle incrementing a true array */
 		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
 		{
 			cur_elem = 0;
@@ -1411,7 +2341,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 		if (!rolled)
 			return true;
 
-		/* Need to advance next array key, if any */
+		/* Array rolled over.  Need to advance next array key, if any. */
 	}
 
 	/*
@@ -1429,6 +2359,95 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	return false;
 }
 
+/*
+ * _bt_advance_skip_array_key_increment() -- increment a skip scan array
+ *
+ * Returns true when the skip array was successfully incremented to the next
+ * value in the current scan direction, dir.  Otherwise handles roll over by
+ * setting array to its final element for the current scan direction.
+ */
+static bool
+_bt_advance_skip_array_key_increment(Relation rel, ScanDirection dir,
+									 BTArrayKeyInfo *array, ScanKey skey,
+									 FmgrInfo *orderproc)
+{
+	Datum		sk_argument = skey->sk_argument;
+	bool		sk_isnull = (skey->sk_flags & SK_ISNULL) != 0;
+	int			compare;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->num_elems == -1);
+
+	if (ScanDirectionIsForward(dir))
+	{
+		/* high_elem is final non-NULL element in current scan direction */
+		compare = _bt_compare_array_skey(orderproc,
+										 array->sksup.high_elem, false,
+										 sk_argument, sk_isnull,
+										 skey);
+		if (compare > 0)
+		{
+			/* Increment non-NULL element to next non-NULL element */
+			_bt_scankey_increment(rel, skey, array);
+
+			return true;
+		}
+		else if (compare == 0 && array->null_elem &&
+				 !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/*
+			 * Existing sk_argument was already equal to high_elem.  Increment
+			 * from high_elem to final NULL element (without calling opclass
+			 * support function, which doesn't know how to handle NULLs).
+			 */
+			_bt_scankey_set_isnull(rel, skey, array);
+
+			return true;
+		}
+
+		/* Exhausted all array elements in current scan direction */
+	}
+	else
+	{
+		/* low_elem is final non-NULL element in current scan direction */
+		compare = _bt_compare_array_skey(orderproc,
+										 array->sksup.low_elem, false,
+										 sk_argument, sk_isnull,
+										 skey);
+		if (compare < 0)
+		{
+			/* Decrement non-NULL element to previous non-NULL element */
+			_bt_scankey_decrement(rel, skey, array);
+
+			return true;
+		}
+		else if (compare == 0 && array->null_elem &&
+				 (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/*
+			 * Existing sk_argument was already equal to low_elem.  Decrement
+			 * from low_elem to final NULL element (without calling opclass
+			 * support function, which doesn't know how to handle NULLs).
+			 */
+			_bt_scankey_set_isnull(rel, skey, array);
+
+			return true;
+		}
+
+		/* Exhausted all array elements in current scan direction */
+	}
+
+	/*
+	 * Skip array rolls over.  Start over at the array's lowest sorting value
+	 * (or its highest value, for backward scans).
+	 */
+	_bt_scankey_set_low_or_high(rel, skey, array, ScanDirectionIsForward(dir));
+
+	/* Caller must consider earlier/more significant arrays in turn */
+	return false;
+}
+
 /*
  * _bt_rewind_nonrequired_arrays() -- Rewind non-required arrays
  *
@@ -1485,6 +2504,8 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
+		Assert(array->num_elems > 0);	/* No skipping of non-required arrays */
+
 		if (ScanDirectionIsForward(dir))
 			first_elem_dir = 0;
 		else
@@ -1558,6 +2579,8 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 	for (int ikey = sktrig; ikey < so->numberOfKeys; ikey++)
 	{
 		ScanKey		cur = so->keyData + ikey;
+		Datum		sk_argument = cur->sk_argument;
+		bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
 		Datum		tupdatum;
 		bool		tupnull;
 		int32		result;
@@ -1621,7 +2644,8 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		result = _bt_compare_array_skey(&so->orderProcs[ikey],
 										tupdatum, tupnull,
-										cur->sk_argument, cur);
+										sk_argument, sk_isnull,
+										cur);
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1954,18 +2978,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1990,18 +3005,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2019,15 +3025,27 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * Skip array.  "Binary search" by checking if tupdatum/tupnull
+			 * are within the low_value/high_value range of the skip array.
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
+			Datum		sk_argument = cur->sk_argument;
+			bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
+
 			Assert(sktrig_required && required);
 
 			/*
@@ -2041,7 +3059,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 */
 			result = _bt_compare_array_skey(&so->orderProcs[ikey],
 											tupdatum, tupnull,
-											cur->sk_argument, cur);
+											sk_argument, sk_isnull, cur);
 		}
 
 		/*
@@ -2061,6 +3079,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * its final element is.  Once outside the loop we'll then "increment
 		 * this array's set_elem" by calling _bt_advance_array_keys_increment.
 		 * That way the process rolls over to higher order arrays as needed.
+		 * The skip array case will set the array's scan key to the final
+		 * valid element for the current scan direction, which is equivalent
+		 * (when we have a real set_elem "match" it's just the final element
+		 * in the current scan direction).
 		 *
 		 * Under this scheme any required arrays only ever ratchet forwards
 		 * (or backwards), and always do so to the maximum possible extent
@@ -2100,11 +3122,62 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+
+		if (!array)
+			continue;			/* no element to set in non-array */
+
+		/* Conventional arrays have a valid set_elem for us to advance to */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * Conceptually, skip arrays also have array elements.  The actual
+		 * elements/values are generated procedurally and on demand.
+		 */
+		Assert(cur->sk_flags & SK_BT_SKIP);
+		Assert(array->num_elems == -1);
+		Assert(required);
+
+		if (result == 0)
+		{
+			/*
+			 * Anything within the range of possible element values is treated
+			 * as "a match for one of the array's elements".  Store the next
+			 * scan key argument value by taking a copy of the tupdatum value
+			 * from caller's tuple (or set scan key IS NULL when tupnull, iff
+			 * the array's range of possible elements covers NULL).
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
+		}
+		else if (beyond_end_advance)
+		{
+			/*
+			 * We need to set the array element to the final "element" in the
+			 * current scan direction for "beyond end of array element" array
+			 * advancement.  See above for an explanation.
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else
+		{
+			/*
+			 * The closest matching element is the lowest element; even that
+			 * still puts us ahead of caller's tuple in the key space.  This
+			 * process has to carry to any lower-order arrays.  See above for
+			 * an explanation.
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
 		}
 	}
 
@@ -2550,9 +3623,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	int16	   *indoption = scan->indexRelation->rd_indoption;
 	int			new_numberOfKeys;
 	int			numberOfEqualCols;
-	ScanKey		inkeys;
-	ScanKey		outkeys;
-	ScanKey		cur;
+	ScanKey		inputsk;
 	BTScanKeyPreproc xform[BTMaxStrategyNumber];
 	bool		test_result;
 	int			i,
@@ -2584,7 +3655,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		return;					/* done if qual-less scan */
 
 	/* If any keys are SK_SEARCHARRAY type, set up array-key info */
-	arrayKeyData = _bt_preprocess_array_keys(scan);
+	arrayKeyData = _bt_preprocess_array_keys(scan, &numberOfKeys);
 	if (!so->qual_ok)
 	{
 		/* unmatchable array, so give up */
@@ -2598,32 +3669,38 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	 */
 	if (arrayKeyData)
 	{
-		inkeys = arrayKeyData;
+		inputsk = arrayKeyData;
 
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
 	}
 	else
-		inkeys = scan->keyData;
+		inputsk = scan->keyData;
+
+	/*
+	 * Now that we have an estimate of the number of output scan keys
+	 * (including any skip array scan keys), allocate space for them
+	 */
+	if (so->keyData != NULL)
+		pfree(so->keyData);
+	so->keyData = palloc(sizeof(ScanKeyData) * numberOfKeys);
 
-	outkeys = so->keyData;
-	cur = &inkeys[0];
 	/* we check that input keys are correctly ordered */
-	if (cur->sk_attno < 1)
+	if (inputsk->sk_attno < 1)
 		elog(ERROR, "btree index keys must be ordered by attribute");
 
 	/* We can short-circuit most of the work if there's just one key */
 	if (numberOfKeys == 1)
 	{
 		/* Apply indoption to scankey (might change sk_strategy!) */
-		if (!_bt_fix_scankey_strategy(cur, indoption))
+		if (!_bt_fix_scankey_strategy(inputsk, indoption))
 			so->qual_ok = false;
-		memcpy(outkeys, cur, sizeof(ScanKeyData));
+		memcpy(so->keyData, inputsk, sizeof(ScanKeyData));
 		so->numberOfKeys = 1;
 		/* We can mark the qual as required if it's for first index col */
-		if (cur->sk_attno == 1)
-			_bt_mark_scankey_required(outkeys);
+		if (inputsk->sk_attno == 1)
+			_bt_mark_scankey_required(so->keyData);
 		if (arrayKeyData)
 		{
 			/*
@@ -2631,8 +3708,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * (we'll miss out on the single value array transformation, but
 			 * that's not nearly as important when there's only one scan key)
 			 */
-			Assert(cur->sk_flags & SK_SEARCHARRAY);
-			Assert(cur->sk_strategy != BTEqualStrategyNumber ||
+			Assert(so->keyData[0].sk_flags & SK_SEARCHARRAY);
+			Assert(so->keyData[0].sk_strategy != BTEqualStrategyNumber ||
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
@@ -2660,12 +3737,12 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	 * handle after-last-key processing.  Actual exit from the loop is at the
 	 * "break" statement below.
 	 */
-	for (i = 0;; cur++, i++)
+	for (i = 0;; inputsk++, i++)
 	{
 		if (i < numberOfKeys)
 		{
 			/* Apply indoption to scankey (might change sk_strategy!) */
-			if (!_bt_fix_scankey_strategy(cur, indoption))
+			if (!_bt_fix_scankey_strategy(inputsk, indoption))
 			{
 				/* NULL can't be matched, so give up */
 				so->qual_ok = false;
@@ -2677,12 +3754,12 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		 * If we are at the end of the keys for a particular attr, finish up
 		 * processing and emit the cleaned-up keys.
 		 */
-		if (i == numberOfKeys || cur->sk_attno != attno)
+		if (i == numberOfKeys || inputsk->sk_attno != attno)
 		{
 			int			priorNumberOfEqualCols = numberOfEqualCols;
 
 			/* check input keys are correctly ordered */
-			if (i < numberOfKeys && cur->sk_attno < attno)
+			if (i < numberOfKeys && inputsk->sk_attno < attno)
 				elog(ERROR, "btree index keys must be ordered by attribute");
 
 			/*
@@ -2741,7 +3818,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
+						Assert(!array || array->num_elems > 0 ||
+							   array->num_elems == -1);
 						xform[j].skey = NULL;
 						xform[j].ikey = -1;
 					}
@@ -2786,7 +3864,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			}
 
 			/*
-			 * Emit the cleaned-up keys into the outkeys[] array, and then
+			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
 			 */
@@ -2794,7 +3872,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			{
 				if (xform[j].skey)
 				{
-					ScanKey		outkey = &outkeys[new_numberOfKeys++];
+					ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
 					memcpy(outkey, xform[j].skey, sizeof(ScanKeyData));
 					if (arrayKeyData)
@@ -2811,19 +3889,19 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				break;
 
 			/* Re-initialize for new attno */
-			attno = cur->sk_attno;
+			attno = inputsk->sk_attno;
 			memset(xform, 0, sizeof(xform));
 		}
 
 		/* check strategy this key's operator corresponds to */
-		j = cur->sk_strategy - 1;
+		j = inputsk->sk_strategy - 1;
 
 		/* if row comparison, push it directly to the output array */
-		if (cur->sk_flags & SK_ROW_HEADER)
+		if (inputsk->sk_flags & SK_ROW_HEADER)
 		{
-			ScanKey		outkey = &outkeys[new_numberOfKeys++];
+			ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
-			memcpy(outkey, cur, sizeof(ScanKeyData));
+			memcpy(outkey, inputsk, sizeof(ScanKeyData));
 			if (arrayKeyData)
 				keyDataMap[new_numberOfKeys - 1] = i;
 			if (numberOfEqualCols == attno - 1)
@@ -2837,19 +3915,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			continue;
 		}
 
-		/*
-		 * Does this input scan key require further processing as an array?
-		 */
-		if (cur->sk_strategy == InvalidStrategy)
-		{
-			/* _bt_preprocess_array_keys marked this array key redundant */
-			Assert(arrayKeyData);
-			Assert(cur->sk_flags & SK_SEARCHARRAY);
-			continue;
-		}
-
-		if (cur->sk_strategy == BTEqualStrategyNumber &&
-			(cur->sk_flags & SK_SEARCHARRAY))
+		if (inputsk->sk_strategy == BTEqualStrategyNumber &&
+			(inputsk->sk_flags & SK_SEARCHARRAY))
 		{
 			/* _bt_preprocess_array_keys kept this array key */
 			Assert(arrayKeyData);
@@ -2863,7 +3930,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		if (xform[j].skey == NULL)
 		{
 			/* nope, so this scan key wins by default (at least for now) */
-			xform[j].skey = cur;
+			xform[j].skey = inputsk;
 			xform[j].ikey = i;
 			xform[j].arrayidx = arrayidx;
 		}
@@ -2881,7 +3948,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/*
 				 * Have to set up array keys
 				 */
-				if ((cur->sk_flags & SK_SEARCHARRAY))
+				if ((inputsk->sk_flags & SK_SEARCHARRAY))
 				{
 					array = &so->arrayKeys[arrayidx - 1];
 					orderproc = so->orderProcs + i;
@@ -2909,13 +3976,15 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				 */
 			}
 
-			if (_bt_compare_scankey_args(scan, cur, cur, xform[j].skey,
-										 array, orderproc, &test_result))
+			if (_bt_compare_scankey_args(scan, inputsk, inputsk,
+										 xform[j].skey, array, orderproc,
+										 &test_result))
 			{
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
+					Assert(!array || array->num_elems > 0 ||
+						   array->num_elems == -1);
 
 					/*
 					 * New key is more restrictive, and so replaces old key...
@@ -2923,7 +3992,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 					if (j != (BTEqualStrategyNumber - 1) ||
 						!(xform[j].skey->sk_flags & SK_SEARCHARRAY))
 					{
-						xform[j].skey = cur;
+						xform[j].skey = inputsk;
 						xform[j].ikey = i;
 						xform[j].arrayidx = arrayidx;
 					}
@@ -2936,7 +4005,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 						 * scan key.  _bt_compare_scankey_args expects us to
 						 * always keep arrays (and discard non-arrays).
 						 */
-						Assert(!(cur->sk_flags & SK_SEARCHARRAY));
+						Assert(!(inputsk->sk_flags & SK_SEARCHARRAY));
 					}
 				}
 				else if (j == (BTEqualStrategyNumber - 1))
@@ -2959,14 +4028,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				 * even with incomplete opfamilies.  _bt_advance_array_keys
 				 * depends on this.
 				 */
-				ScanKey		outkey = &outkeys[new_numberOfKeys++];
+				ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
 				memcpy(outkey, xform[j].skey, sizeof(ScanKeyData));
 				if (arrayKeyData)
 					keyDataMap[new_numberOfKeys - 1] = xform[j].ikey;
 				if (numberOfEqualCols == attno - 1)
 					_bt_mark_scankey_required(outkey);
-				xform[j].skey = cur;
+				xform[j].skey = inputsk;
 				xform[j].ikey = i;
 				xform[j].arrayidx = arrayidx;
 			}
@@ -3057,10 +4126,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3135,6 +4205,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Don't allow skip array to generate IS NULL scan key/element */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3208,6 +4294,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3380,13 +4467,6 @@ _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption)
 		return true;
 	}
 
-	if (skey->sk_strategy == InvalidStrategy)
-	{
-		/* Already-eliminated array scan key; don't need to fix anything */
-		Assert(skey->sk_flags & SK_SEARCHARRAY);
-		return true;
-	}
-
 	/* Adjust strategy for DESC, if we didn't already */
 	if ((addflags & SK_BT_DESC) && !(skey->sk_flags & SK_BT_DESC))
 		skey->sk_strategy = BTCommuteStrategyNumber(skey->sk_strategy);
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 610ccf2f7..49d792c1c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -96,6 +96,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 9c854e0e5..ea3d0f4b5 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,39 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	Assert(dexisting > DATEVAL_NOBEGIN);
+
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	Assert(dexisting < DATEVAL_NOEND);
+
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 date_finite(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index 48dbcf59a..4f82f3169 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -83,6 +83,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 5f5d7959d..c1df7be9f 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -6800,6 +6800,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		found_skip;
 	bool		found_saop;
 	bool		found_is_null_op;
 	double		num_sa_scans;
@@ -6825,6 +6826,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	found_skip = false;
 	found_saop = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
@@ -6833,15 +6835,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
 		ListCell   *lc2;
 
+		/*
+		 * XXX For now we just cost skip scans via generic rules: make a
+		 * uniform assumption that there will be 10 primitive index scans per
+		 * skipped attribute, relying on the "1/3 of all index pages" cap that
+		 * this costing has used since Postgres 17.  Also assume that skipping
+		 * won't take place for an index that has fewer than 100 pages.
+		 *
+		 * The current approach to costing leaves much to be desired, but is
+		 * at least better than nothing at all (keeping the code as it is on
+		 * HEAD just makes testing and review inconvenient).
+		 */
 		if (indexcol != iclause->indexcol)
 		{
 			/* Beginning of a new column's quals */
 			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			{
+				found_skip = true;	/* skip when no '=' qual for indexcol */
+				if (index->pages < 100)
+					break;
+				num_sa_scans += 10;
+			}
 			eqQualHere = false;
 			indexcol++;
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			{
+				/* no quals at all for indexcol */
+				found_skip = true;
+				if (index->pages < 100)
+					break;
+				num_sa_scans += 10 * (indexcol - iclause->indexcol);
+				continue;
+			}
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6914,6 +6939,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..9665e4985
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,54 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scans.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include <limits.h>
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		Datum		low_elem = sksup->low_elem;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->high_elem = low_elem;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 45eb1b2fe..a9222f896 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -390,6 +393,68 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	Assert(false);
+
+	return UUIDPGetDatum(uuid);
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	Assert(false);
+
+	return UUIDPGetDatum(uuid);
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 46c258be2..b84ec2298 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -3523,6 +3524,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..9662fb2ba 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -583,6 +583,19 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure via an index <quote>skip scan</quote>.  The
+     APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 22d8ad1aa..f17dd3456 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1056,7 +1063,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal);
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1069,7 +1077,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal);
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1082,7 +1091,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal);
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..8b6b775c1 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 3bbe4c5f9..a8d5be6c1 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5138,9 +5138,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..4246afefd 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 61ad417cd..2aa7c871c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2650,6 +2651,8 @@ SingleBoundSortItem
 SinglePartitionSpec
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.1

#2Aleksander Alekseev
aleksander@timescale.com
In reply to: Peter Geoghegan (#1)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Hi Peter,

Attached is a POC patch that adds skip scan to nbtree. The patch
teaches nbtree index scans to efficiently use a composite index on
'(a, b)' for queries with a predicate such as "WHERE b = 5". This is
feasible in cases where the total number of distinct values in the
column 'a' is reasonably small (think tens or hundreds, perhaps even
thousands for very large composite indexes).

[...]

Thoughts?

Many thanks for working on this. I believe it is an important feature
and it would be great to deliver it during the PG18 cycle.

I experimented with the patch and here are the results I got so far.

Firstly, it was compiled on Intel MacOS and ARM Linux. All the tests
pass just fine.

Secondly, I tested the patch manually using a release build on my
Raspberry Pi 5 and the GUCs that can be seen in [1]https://github.com/afiskon/pgscripts/blob/master/single-install-meson.sh.

Test 1 - simple one.

```
CREATE TABLE test1(c char, n bigint);
CREATE INDEX test1_idx ON test1 USING btree(c,n);

INSERT INTO test1
SELECT chr(ascii('a') + random(0,2)) AS c,
random(0, 1_000_000_000) AS n
FROM generate_series(0, 1_000_000);

EXPLAIN [ANALYZE] SELECT COUNT(*) FROM test1 WHERE n > 900_000_000;
```

Test 2 - a more complicated one.

```
CREATE TABLE test2(c1 char, c2 char, n bigint);
CREATE INDEX test2_idx ON test2 USING btree(c1,c2,n);

INSERT INTO test2
SELECT chr(ascii('a') + random(0,2)) AS c1,
chr(ascii('a') + random(0,2)) AS c2,
random(0, 1_000_000_000) AS n
FROM generate_series(0, 1_000_000);

EXPLAIN [ANALYZE] SELECT COUNT(*) FROM test2 WHERE n > 900_000_000;
```

Test 3 - to see how it works with covering indexes.

```
CREATE TABLE test3(c char, n bigint, s text DEFAULT 'text_value' || n);
CREATE INDEX test3_idx ON test3 USING btree(c,n) INCLUDE(s);

INSERT INTO test3
SELECT chr(ascii('a') + random(0,2)) AS c,
random(0, 1_000_000_000) AS n,
'text_value_' || random(0, 1_000_000_000) AS s
FROM generate_series(0, 1_000_000);

EXPLAIN [ANALYZE] SELECT s FROM test3 WHERE n < 1000;
```

In all the cases the patch worked as expected.

I noticed that with the patch we choose Index Only Scans for Test 1
and without the patch - Parallel Seq Scan. However the Parallel Seq
Scan is 2.4 times faster. Before the patch the query takes 53 ms,
after the patch - 127 ms. I realize this could be just something
specific to my hardware and/or amount of data.

Do you think this is something that was expected or something worth
investigating further?

I haven't looked at the code yet.

[1]: https://github.com/afiskon/pgscripts/blob/master/single-install-meson.sh

--
Best regards,
Aleksander Alekseev

In reply to: Aleksander Alekseev (#2)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Jul 2, 2024 at 8:53 AM Aleksander Alekseev
<aleksander@timescale.com> wrote:

CREATE TABLE test1(c char, n bigint);
CREATE INDEX test1_idx ON test1 USING btree(c,n);

The type "char" (note the quotes) is different from char(1). It just
so happens that v1 has support for skipping attributes that use the
default opclass for "char", without support for char(1).

If you change your table definition to CREATE TABLE test1(c "char", n
bigint), then your example queries can use the optimization. This
makes a huge difference.

EXPLAIN [ANALYZE] SELECT COUNT(*) FROM test1 WHERE n > 900_000_000;

For example, this first test query goes from needing a full index scan
that has 5056 buffer hits to a skip scan that requires only 12 buffer
hits.

I noticed that with the patch we choose Index Only Scans for Test 1
and without the patch - Parallel Seq Scan. However the Parallel Seq
Scan is 2.4 times faster. Before the patch the query takes 53 ms,
after the patch - 127 ms.

I'm guessing that it's actually much faster once you change the
leading column to the "char" type/default opclass.

I realize this could be just something
specific to my hardware and/or amount of data.

The selfuncs.c costing current has a number of problems.

One problem is that it doesn't know that some opclasses/types don't
support skipping at all. That particular problem should be fixed on
the nbtree side; nbtree should support skipping regardless of the
opclass that the skipped attribute uses (while still retaining the new
opclass support functions for a subset of types where we expect it to
make skip scans somewhat faster).

--
Peter Geoghegan

In reply to: Peter Geoghegan (#3)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Jul 2, 2024 at 9:30 AM Peter Geoghegan <pg@bowt.ie> wrote:

EXPLAIN [ANALYZE] SELECT COUNT(*) FROM test1 WHERE n > 900_000_000;

For example, this first test query goes from needing a full index scan
that has 5056 buffer hits to a skip scan that requires only 12 buffer
hits.

Actually, looks like that's an invalid result. The "char" opclass
support function appears to have bugs.

My testing totally focussed on types like integer, date, and UUID. The
"char" opclass was somewhat of an afterthought. Will fix "char" skip
support for v2.

--
Peter Geoghegan

In reply to: Peter Geoghegan (#4)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Jul 2, 2024 at 9:40 AM Peter Geoghegan <pg@bowt.ie> wrote:

On Tue, Jul 2, 2024 at 9:30 AM Peter Geoghegan <pg@bowt.ie> wrote:

EXPLAIN [ANALYZE] SELECT COUNT(*) FROM test1 WHERE n > 900_000_000;

For example, this first test query goes from needing a full index scan
that has 5056 buffer hits to a skip scan that requires only 12 buffer
hits.

Actually, looks like that's an invalid result. The "char" opclass
support function appears to have bugs.

Attached v2 fixes this bug. The problem was that the skip support
function used by the "char" opclass assumed signed char comparisons,
even though the authoritative B-Tree comparator (support function 1)
uses signed comparisons (via uint8 casting). A simple oversight. Your
test cases will work with this v2, provided you use "char" (instead of
unadorned char) in the create table statements.

Another small change in v2: I added a DEBUG2 message to nbtree
preprocessing, indicating the number of attributes that we're going to
skip. This provides an intuitive way to see whether the optimizations
are being applied in the first place. That should help to avoid
further confusion like this as the patch continues to evolve.

Support for char(1) doesn't seem feasible within the confines of a
skip support routine. Just like with text (which I touched on in the
introductory email), this will require teaching nbtree to perform
explicit next-key probes. An approach based on explicit probes is
somewhat less efficient in some cases, but it should always work. It's
impractical to write opclass support that (say) increments a char
value 'a' to 'b'. Making that approach work would require extensive
cooperation from the collation provider, and some knowledge of
encoding, which just doesn't make sense (if it's possible at all). I
don't have the problem with "char" because it isn't a collatable type
(it is essentially the same thing as an uint8 integer type, except
that it outputs printable ascii characters).

FWIW, your test cases don't seem like particularly good showcases for
the patch. The queries you came up with require a relatively large
amount of random I/O when accessing the heap, which skip scan will
never help with -- so skip scan is a small win (at least relative to
an unoptimized full index scan). Obviously, no skip scan can ever
avoid any required heap accesses compared to a naive full index scan
(loose index scan *does* have that capability, which is possible only
because it applies semantic information in a way that's very
different).

FWIW, a more sympathetic version of your test queries would have
involved something like "WHERE n = 900_500_000". That would allow the
implementation to perform a series of *selective* primitive index
scans (one primitive index scan per "c" column/char grouping). That
change has the effect of allowing the scan to skip over many
irrelevant leaf pages, which is of course the whole point of skip
scan. It also makes the scan will require far fewer heap accesses, so
heap related costs no longer drown out the nbtree improvements.

--
Peter Geoghegan

Attachments:

v2-0001-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v2-0001-Add-skip-scan-to-nbtree.patchDownload
From d41c1da841e4ab6245caff02d17b945f8346b47b Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v2] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on an index (a, b) for queries with a predicate such as "WHERE b = 5".
This is useful in cases where the total number of distinct values in the
column 'a' is reasonably small (think hundreds, possibly thousands).

In effect, a skip scan treats the composite index on (a, b) as if it was
a series of disjunct subindexes -- one subindex per distinct 'a' value.
We exhaustively "search every subindex" using a qual that behaves like
"WHERE a = ANY(<every possible 'a' value>) AND b = 5".
---
 src/include/access/nbtree.h                 |   16 +-
 src/include/catalog/pg_amproc.dat           |   16 +
 src/include/catalog/pg_proc.dat             |   24 +
 src/include/utils/skipsupport.h             |  140 ++
 src/backend/access/nbtree/nbtcompare.c      |  201 +++
 src/backend/access/nbtree/nbtree.c          |   10 +-
 src/backend/access/nbtree/nbtutils.c        | 1399 ++++++++++++++++---
 src/backend/access/nbtree/nbtvalidate.c     |    4 +
 src/backend/commands/opclasscmds.c          |   25 +
 src/backend/utils/adt/Makefile              |    1 +
 src/backend/utils/adt/date.c                |   34 +
 src/backend/utils/adt/meson.build           |    1 +
 src/backend/utils/adt/selfuncs.c            |   30 +-
 src/backend/utils/adt/skipsupport.c         |   54 +
 src/backend/utils/adt/uuid.c                |   65 +
 src/backend/utils/misc/guc_tables.c         |   12 +
 doc/src/sgml/btree.sgml                     |   13 +
 doc/src/sgml/xindex.sgml                    |   16 +-
 src/test/regress/expected/alter_generic.out |    6 +-
 src/test/regress/expected/psql.out          |    3 +-
 src/test/regress/sql/alter_generic.sql      |    2 +-
 src/tools/pgindent/typedefs.list            |    3 +
 22 files changed, 1899 insertions(+), 176 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 749304334..81e99fcc1 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1032,9 +1034,15 @@ typedef BTScanPosData *BTScanPos;
 typedef struct BTArrayKeyInfo
 {
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* State used by standard arrays that store elements in memory */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* State used by skip arrays, which generate elements procedurally */
+	SkipSupportData sksup;		/* opclass skip scan support */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1123,6 +1131,7 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* SK_SEARCHARRAY skip scan key */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1159,6 +1168,9 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameter (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index f639c3a6a..2a8f6f3f1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -122,6 +128,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +149,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +170,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +205,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +275,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d4ac578ae..d02dd1a0c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2214,6 +2229,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4368,6 +4386,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -9175,6 +9196,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..a71a624d0
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,140 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scans.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * This happens at the point where the scan determines that another primitive
+ * index scan is required.  The next value is used (in combination with at
+ * least one additional lower-order non-skip key, taken from the SQL query) to
+ * relocate the scan, skipping over many irrelevant leaf pages in the process.
+ *
+ * There are many data types/opclasses where implementing a skip support
+ * scheme is inherently impossible (or at least impractical).  Obviously, it
+ * would be wrong if the "next" value generated by an opclass was actually
+ * after the true next value (any index tuples with the true next value would
+ * be overlooked by the index scan).  This partly explains why opclasses are
+ * under no obligation to implement skip support: a continuous type may have
+ * no way of generating a useful next value.
+ *
+ * Skip scan generally works best with discrete types such as integer, date,
+ * and boolean: types where we expect indexes to contain large groups of
+ * contiguous values (in respect of the leading/skipped index attribute).
+ * When gaps/discontinuities are naturally rare (e.g., a leading identity
+ * column in a composite index, a date column preceding a product_id column),
+ * then it makes sense for the skip scan to optimistically assume that the
+ * next distinct indexable value will find directly matching index tuples.
+ * The B-Tree code can fall back on explicit next-key probes for any opclass
+ * that doesn't include a skip support function, but it's best to provide skip
+ * support whenever possible.  The B-Tree code assumes that it's always better
+ * to use the opclass skip support routine where available.
+ *
+ * When a skip scan "bets" that the next indexable value will find an exact
+ * match, there is significant upside, without any accompanying downside.
+ * When this optimistic strategy works out, the scan avoids the cost of an
+ * explicit probe (used in the no-skip-support case to determine the true next
+ * value in the index's skip attribute).  When the strategy doesn't work out,
+ * then the scan is no worse off than it would have been without skip support.
+ * The explicit next-key probes used by B-Tree skip scan's fallback path are
+ * very similar to "failed" optimistic searches for the next indexable value
+ * (the next value according to the opclass skip support routine).
+ *
+ * (FIXME Actually, nbtree does no such thing right now, which is considered a
+ * blocker to commit.)
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called.
+ * If an opclass can only set some of the fields, then it cannot safely
+ * provide a skip support routine (and so must rely on the fallback strategy
+ * used by continuous types, such as numeric).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming standard ascending
+	 * order).  This helps the B-Tree code with finding its initial position
+	 * at the leaf level (during the skip scan's first primitive index scan).
+	 * In other words, it gives the B-Tree code a useful value to start from,
+	 * before any data has been read from the index.
+	 *
+	 * low_elem and high_elem can also be used to prove that a qual is
+	 * unsatisfiable in certain cross-type scenarios.
+	 *
+	 * low_elem and high_elem are also used by skip scans to determine when
+	 * they've reached the final possible value (in the current direction).
+	 * It's typical for the scan to run out of leaf pages before it runs out
+	 * of unscanned indexable values, but it's still useful for the scan to
+	 * have a way to recognize when it has reached the last possible value
+	 * (this saves us a useless probe that just lands on the final leaf page).
+	 *
+	 * Note: the logic for determining that the scan has reached the final
+	 * possible value naturally belongs in the B-Tree code.  The final value
+	 * isn't necessarily the original high_elem/low_elem set by the opclass.
+	 * In particular, it'll be a lower/higher value when B-Tree preprocessing
+	 * determines that the true range of possible values should be restricted,
+	 * due to the presence of an inequality applied to the index's skipped
+	 * attribute.  These are range skip scans.
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (in the case of pass-by-reference
+	 * types).  It's not okay for these functions to leak any memory.
+	 *
+	 * Both decrement and increment callbacks are guaranteed to never be
+	 * called with a NULL "existing" arg.  (In general it is the B-Tree code's
+	 * job to worry about NULLs, and about whether indexed values are stored
+	 * in ASC order or DESC order.)
+	 *
+	 * The decrement callback is guaranteed to only be called with an
+	 * "existing" value that's strictly > the low_elem set by the opclass.
+	 * Similarly, the increment callback is guaranteed to only be called with
+	 * an "existing" value that's strictly < the high_elem set by the opclass.
+	 * Consequently, opclasses don't have to deal with "overflow" themselves
+	 * (though asserting that the B-Tree code got it right is a good idea).
+	 *
+	 * It's quite possible (and very common) for the B-Tree skip scan caller's
+	 * "existing" datum to just be a straight copy of a value that it copied
+	 * from the index.  Operator classes must be liberal in accepting every
+	 * possible representational variation within the underlying data type.
+	 * Opclasses don't have to preserve whatever semantically insignificant
+	 * information the data type might be carrying around, though.
+	 *
+	 * Note: < and > are defined by the opclass's ORDER proc in the usual way.
+	 */
+	Datum		(*decrement) (Relation rel, Datum existing);
+	Datum		(*increment) (Relation rel, Datum existing);
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..48a877613 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,39 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	Assert(bexisting == true);
+
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	Assert(bexisting == false);
+
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +139,39 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	Assert(iexisting > PG_INT16_MIN);
+
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	Assert(iexisting < PG_INT16_MAX);
+
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +195,39 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	Assert(iexisting > PG_INT32_MIN);
+
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	Assert(iexisting < PG_INT32_MAX);
+
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +271,39 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	Assert(iexisting > PG_INT64_MIN);
+
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	Assert(iexisting < PG_INT64_MAX);
+
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +425,39 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	Assert(oexisting > InvalidOid);
+
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	Assert(oexisting < OID_MAX);
+
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +491,38 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	Assert(cexisting > 0);
+
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	Assert(cexisting < UCHAR_MAX);
+
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 686a3206f..9c9cd48f7 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -324,11 +324,8 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	so = (BTScanOpaque) palloc(sizeof(BTScanOpaqueData));
 	BTScanPosInvalidate(so->currPos);
 	BTScanPosInvalidate(so->markPos);
-	if (scan->numberOfKeys > 0)
-		so->keyData = (ScanKey) palloc(scan->numberOfKeys * sizeof(ScanKeyData));
-	else
-		so->keyData = NULL;
 
+	so->keyData = NULL;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->arrayKeys = NULL;
@@ -408,6 +405,11 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 				scan->numberOfKeys * sizeof(ScanKeyData));
 	so->numberOfKeys = 0;		/* until _bt_preprocess_keys sets it */
 	so->numArrayKeys = 0;		/* ditto */
+
+	/* Release private storage allocated in previous btrescan, if any */
+	if (so->keyData != NULL)
+		pfree(so->keyData);
+	so->keyData = NULL;
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index d6de2072d..f4442d014 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -28,10 +28,44 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/skipsupport.h"
+
+/*
+ * GUC parameter (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.
+ *
+ * For example, setting skipscan_prefix_cols=1 before an index scan with qual
+ * "WHERE b = 1 AND c > 42" will make us generate a skip scan key on the
+ * column 'a' (which is attnum 1) only, preventing us from adding one for the
+ * column 'c' (and so 'c' will still have an inequality scan key, required in
+ * only one direction -- 'c' won't be output as a "range" skip key/array).
+ *
+ * The same scan keys will be output when skipscan_prefix_cols=2, given the
+ * same query/qual, since we naturally get a required equality scan key on 'b'
+ * from the input scan keys (provided we at least manage to add a skip scan
+ * key on 'a' that "anchors its required-ness" to the 'b' scan key.)
+ *
+ * When skipscan_prefix_cols is set to the number of key columns in the index,
+ * we're as aggressive as possible about adding skip scan arrays/scan keys.
+ * This is the current default behavior, and the behavior we're targeting for
+ * the committed patch (if there are slowdowns from being maximally aggressive
+ * here then the likely solution is to make _bt_advance_array_keys adaptive,
+ * rather than trying to predict what will work during preprocessing).
+ */
+int			skipscan_prefix_cols;
 
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support */
+	Oid			eq_op;			/* InvalidOid means don't skip */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -62,18 +96,49 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
-static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan);
+static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts);
+static bool _bt_skip_support(Relation rel, int add_skip_attno,
+							 BTSkipPreproc *skipatts);
+static inline Datum _bt_apply_decrement(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array);
+static inline Datum _bt_apply_increment(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
-										   Datum arrdatum, ScanKey cur);
+										   Datum arrdatum, bool arrnull,
+										   ScanKey cur);
+static void _bt_apply_compare_array(ScanKey arraysk, ScanKey skey,
+									FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_apply_compare_skiparray(IndexScanDesc scan, ScanKey arraysk,
+										ScanKey skey, FmgrInfo *orderproc,
+										FmgrInfo *orderprocp,
+										BTArrayKeyInfo *array, bool *qual_ok);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
+static bool _bt_advance_skip_array_key_increment(Relation rel, ScanDirection dir,
+												 BTArrayKeyInfo *array, ScanKey skey,
+												 FmgrInfo *orderproc);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 										 IndexTuple tuple, TupleDesc tupdesc, int tupnatts,
@@ -251,9 +316,6 @@ _bt_freestack(BTStack stack)
  * It is convenient for _bt_preprocess_keys caller to have to deal with no
  * more than one equality strategy array scan key per index attribute.  We'll
  * always be able to set things up that way when complete opfamilies are used.
- * Eliminated array scan keys can be recognized as those that have had their
- * sk_strategy field set to InvalidStrategy here by us.  Caller should avoid
- * including these in the scan's so->keyData[] output array.
  *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
@@ -261,18 +323,36 @@ _bt_freestack(BTStack stack)
  * preprocessing steps are complete.  This will convert the scan key offset
  * references into references to the scan's so->keyData[] output scan keys.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by later preprocessing on output.
+ * _bt_decide_skipatts decides which attributes receive skip arrays.
+ *
+ * Caller must pass *numberOfKeys to give us a way to change the number of
+ * input scan keys (our output is caller's input).  The returned array can be
+ * smaller than scan->keyData[] when we eliminated a redundant array scan key
+ * (redundant with some other array scan key, for the same attribute).  It can
+ * also be larger when we added a skip array/skip scan key.  Caller uses this
+ * to allocate so->keyData[] for the current btrescan.
+ *
  * Note: the reason we need to return a temp scan key array, rather than just
  * scribbling on scan->keyData, is that callers are permitted to call btrescan
  * without supplying a new set of scankey data.
  */
 static ScanKey
-_bt_preprocess_array_keys(IndexScanDesc scan)
+_bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
+	int			numArrayKeyData = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
-	int			numArrayKeys;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
+	int			numArrayKeys,
+				numSkipArrayKeys,
+				output_ikey = 0;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -280,11 +360,14 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
+	Assert(scan->numberOfKeys);
 
-	/* Quick check to see if there are any array keys */
+	/*
+	 * Quick check to see if there are any array keys, or any missing keys we
+	 * can generate a "skip scan" array key for ourselves
+	 */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
 		cur = &scan->keyData[i];
 		if (cur->sk_flags & SK_SEARCHARRAY)
@@ -300,6 +383,18 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		}
 	}
 
+	/* Consider generating skip arrays, and associated equality scan keys */
+	numSkipArrayKeys = _bt_decide_skipatts(scan, skipatts);
+	if (numSkipArrayKeys)
+	{
+		/* At least one skip array scan key must be added to arrayKeyData[] */
+		numArrayKeys += numSkipArrayKeys;
+		/* output scan key buffer allocation needs space for skip scan keys */
+		numArrayKeyData += numSkipArrayKeys;
+
+		elog(DEBUG2, "skipping %d index attributes", numSkipArrayKeys);
+	}
+
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
@@ -317,19 +412,23 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
-	/* Create modifiable copy of scan->keyData in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
-	memcpy(arrayKeyData, scan->keyData, numberOfKeys * sizeof(ScanKeyData));
+	/* Create output scan keys in the workspace context */
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/*
+	 * Process each array key, and generate skip arrays as needed.  Also copy
+	 * every scan->keyData[] input scan key (whether it's an array or not)
+	 * into the arrayKeyData array we'll return to our caller (barring any
+	 * array scan keys that we could eliminate early through array merging).
+	 */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -345,14 +444,78 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		int			num_nonnulls;
 		int			j;
 
-		cur = &arrayKeyData[i];
-		if (!(cur->sk_flags & SK_SEARCHARRAY))
-			continue;
+		/* Create a skip array and scan key where indicated by skipatts */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* won't skip using this attribute */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[output_ikey];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how the
+			 * consed-up "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[output_ikey], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			output_ikey++;		/* keep this scan key/array */
+		}
 
 		/*
-		 * First, deconstruct the array into elements.  Anything allocated
-		 * here (including a possibly detoasted array value) is in the
-		 * workspace context.
+		 * Copy input scan key into temp arrayKeyData scan key array.  (From
+		 * here on, cur points at our copy of the input scan key.)
+		 */
+		cur = &arrayKeyData[output_ikey];
+		*cur = scan->keyData[input_ikey];
+
+		if (!(cur->sk_flags & SK_SEARCHARRAY))
+		{
+			output_ikey++;		/* keep this non-array scan key */
+			continue;
+		}
+
+		/*
+		 * Deconstruct the array into elements
 		 */
 		arrayval = DatumGetArrayTypeP(cur->sk_argument);
 		/* We could cache this data, but not clear it's worth it */
@@ -406,6 +569,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
+				output_ikey++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -416,6 +580,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
+				output_ikey++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -432,7 +597,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[i], &sortprocp);
+							&so->orderProcs[output_ikey], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -476,11 +641,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					break;
 				}
 
-				/*
-				 * Indicate to _bt_preprocess_keys caller that it must ignore
-				 * this scan key
-				 */
-				cur->sk_strategy = InvalidStrategy;
+				/* Throw away this array */
 				continue;
 			}
 
@@ -511,12 +672,16 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = i;
+		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
 		numArrayKeys++;
+		output_ikey++;			/* keep this scan key/array */
 	}
 
+	/* Set final number of arrayKeyData[] keys, array keys */
+	*numberOfKeys = output_ikey;
 	so->numArrayKeys = numArrayKeys;
 
 	MemoryContextSwitchTo(oldContext);
@@ -624,7 +789,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -685,6 +851,245 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_decide_skipatts() -- set index attributes requiring skip arrays
+ *
+ * _bt_preprocess_array_keys helper function.  Determines which attributes
+ * will require skip arrays/scan keys.  Also sets up skip support function for
+ * each of these attributes.
+ *
+ * This sets up "skip scan".  Adding skip arrays (and associated scan keys)
+ * allows _bt_preprocess_keys to mark lower-order scan keys (copied from the
+ * original scan->keyData[] array in the conventional way) as required.  The
+ * overall effect is to enable skipping over irrelevant sections of the index.
+ *
+ * Return value is the total number of scan keys to add as "input" scan keys
+ * for further processing within _bt_preprocess_keys.
+ */
+static int
+_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts)
+{
+	Relation	rel = scan->indexRelation;
+	ScanKey		inputsk;
+	AttrNumber	attno_inputsk = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numSkipArrayKeys = 0,
+				prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * XXX Don't support system catalogs for now.  Calls to routines like
+	 * get_opfamily_member() are prone to infinite recursion, which we'll need
+	 * to find workaround for (hard-coded lookups?).
+	 */
+	if (IsCatalogRelation(rel))
+		return 0;
+
+	/*
+	 * FIXME Also don't support parallel scans for now.  Must add logic to
+	 * places like _bt_parallel_primscan_schedule so that we account for skip
+	 * arrays when parallel workers serialize their array scan state.
+	 */
+	if (scan->parallel_scan)
+		return 0;
+
+	inputsk = &scan->keyData[0];
+	for (int i = 0;; inputsk++, i++)
+	{
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inputsk
+		 */
+		while (attno_skip < attno_inputsk)
+		{
+			if (!_bt_skip_support(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Opclass lacks a suitable skip support routine.
+				 *
+				 * Return prev_numSkipArrayKeys, so as to avoid including any
+				 * "backfilled" arrays that were supposed to form a contiguous
+				 * group with a skip array on this attribute.  There is no
+				 * benefit to adding backfill skip arrays unless we can do so
+				 * for all attributes (all attributes up to and including the
+				 * one immediately before attno_inputsk).
+				 */
+				return prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			numSkipArrayKeys++;
+			attno_skip++;
+		}
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i >= scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 * (either in part or in whole)
+		 */
+		if (attno_inputsk > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inputsk (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inputsk < inputsk->sk_attno)
+		{
+			prev_numSkipArrayKeys = numSkipArrayKeys;
+
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (!attno_has_equal)
+			{
+				/* Only saw inequalities for the prior attribute */
+				if (_bt_skip_support(rel, attno_skip,
+									 &skipatts[attno_skip - 1]))
+				{
+					/* add a range skip array for this attribute */
+					numSkipArrayKeys++;
+				}
+				else
+					break;
+			}
+			else
+			{
+				/*
+				 * Saw an equality for the prior attribute, so it doesn't need
+				 * a skip array (not even a range skip array).  We'll be able
+				 * to add later skip arrays, too (doesn't matter if the prior
+				 * attribute uses an input opclass without skip support).
+				 */
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inputsk = inputsk->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this scan key's attribute has any equality strategy scan
+		 * keys.
+		 *
+		 * Treat IS NULL scan keys as using equal strategy (they'll be marked
+		 * as using it later on, by _bt_fix_scankey_strategy).
+		 */
+		if (inputsk->sk_strategy == BTEqualStrategyNumber ||
+			(inputsk->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.
+		 *
+		 * We do still backfill skip attributes before the RowCompare, so that
+		 * it can be marked required.  This is similar to what happens when a
+		 * conventional inequality uses an opclass that lacks skip support.
+		 */
+		if (inputsk->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	return numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skip_support() -- set up skip support function in *skipatts
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's skip support routine for caller.  Otherwise returns false.
+ */
+static bool
+_bt_skip_support(Relation rel, int add_skip_attno, BTSkipPreproc *skipatts)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator */
+	skipatts->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										  BTEqualStrategyNumber);
+
+	/*
+	 * We don't expect input opclasses lacking even an equality operator, but
+	 * it's possible.  Deal with it gracefully.
+	 */
+	if (!OidIsValid(skipatts->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	return PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse,
+										 &skipatts->sksup);
+}
+
+/*
+ * _bt_apply_decrement() -- Get a decremented copy of skey's arg
+ *
+ * Note: this wrapper function calls the opclass increment function when the
+ * index stores values in descending order.  We're "logically decrementing" to
+ * the previous value in the key space regardless.
+ */
+static inline Datum
+_bt_apply_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	if (!(skey->sk_flags & SK_BT_DESC))
+		return array->sksup.decrement(rel, skey->sk_argument);
+	else
+		return array->sksup.increment(rel, skey->sk_argument);
+}
+
+/*
+ * _bt_apply_increment() -- Get an incremented copy of skey's arg
+ *
+ * Note: this wrapper function calls the opclass decrement function when the
+ * index stores values in descending order.  We're "logically incrementing" to
+ * the next value in the key space regardless.
+ */
+static inline Datum
+_bt_apply_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	if (!(skey->sk_flags & SK_BT_DESC))
+		return array->sksup.increment(rel, skey->sk_argument);
+	else
+		return array->sksup.decrement(rel, skey->sk_argument);
+}
+
 /*
  * _bt_setup_array_cmp() -- Set up array comparison functions
  *
@@ -979,15 +1384,10 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
@@ -1000,8 +1400,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.  We have to be sure
+	 * that _bt_compare_array_skey/_bt_binsrch_array_skey use the right proc.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1032,11 +1432,46 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	/*
+	 * We have all we need to determine redundancy/contradictoriness.
+	 *
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+		_bt_apply_compare_array(arraysk, skey,
+								orderprocp, array, qual_ok);
+	else
+		_bt_apply_compare_skiparray(scan, arraysk, skey,
+									orderproc, orderprocp,
+									array, qual_ok);
+
+	return true;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when it
+ * is redundant with (or contradicted by) a non-array scalar scan key.
+ *
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ */
+static void
+_bt_apply_compare_array(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1088,8 +1523,152 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
 
-	return true;
+/*
+ * Finish off preprocessing of skip array scan key when it is redundant with
+ * (or contradicted by) a non-array scalar scan key.
+ *
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Arrays used to skip (skip scan/missing key attribute predicates) work by
+ * procedurally generating their elements on the fly.  We must still
+ * "eliminate contradictory elements", but it works a little differently: we
+ * narrow the range of the skip array, such that the array will never
+ * generated contradicted-by-skey elements.
+ *
+ * FIXME Our behavior in scenarios with cross-type operators (range skip scan
+ * cases) is buggy.  We're naively copying datums of a different type from
+ * scalar inequality scan keys into the array's low_value and high_value
+ * fields.  In practice this tends to not visibly break (in practice types
+ * that appear within the same operator family tend to have compatible datum
+ * representations, at least on systems with little-endian byte order).  Put
+ * off dealing with the problem until a later revision of the patch.
+ *
+ * It seems likely that the best way to fix this problem will involve keeping
+ * around the original operator in the BTArrayKeyInfo array struct whenever
+ * we're passed a "redundant" cross-type inequality operator (an approach
+ * involving casts/coercions might be tempting, but seems much too fragile).
+ * We only need to use not-column-input-opclass-type operators for the first
+ * and/or last array elements from the skip array under this scheme; we'll
+ * still mostly be dealing with opcintype-typed datums, copied from the index
+ * (as well as incrementing/decrementing copies of those index tuple datums).
+ * Importantly, this scheme should work just as well with an opfamily that
+ * doesn't even have an orderprocp cross-type ORDER operator to pass us here
+ * (we might even have to keep more than one same-strategy inequality, since
+ * in general _bt_preprocess_keys might not be able to prove which inequality
+ * is redundant).
+ */
+static void
+_bt_apply_compare_skiparray(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							FmgrInfo *orderproc, FmgrInfo *orderprocp,
+							BTArrayKeyInfo *array, bool *qual_ok)
+{
+	Relation	rel = scan->indexRelation;
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	Form_pg_attribute attr = TupleDescAttr(RelationGetDescr(rel),
+										   skey->sk_attno - 1);
+	MemoryContext oldContext;
+	int			cmpresult;
+
+	/*
+	 * We don't expect to have to deal with NULLs in non-array/non-skip scan
+	 * key.  We expect _bt_preprocess_array_keys to avoid generating a skip
+	 * array for an index attribute with an IS NULL input scan key. It will
+	 * still do so in the presence of IS NOT NULL input scan keys, but
+	 * _bt_compare_scankey_args is expected to handle those for us.
+	 */
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(arraysk->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & SK_ISNULL));
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Scalar scan key must be a B-Tree operator, which must always be strict.
+	 * Array shouldn't generate a NULL "array element"/an IS NULL qual.  This
+	 * isn't just an optimization; it's strictly necessary for correctness.
+	 */
+	array->null_elem = false;
+
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+
+			/*
+			 * detect if scan key argument will be < low_value once
+			 * decremented
+			 */
+			cmpresult = _bt_compare_array_skey(orderprocp,
+											   skey->sk_argument, false,
+											   array->sksup.low_elem, false,
+											   arraysk);
+			if (cmpresult <= 0)
+			{
+				/* decrementing would make qual unsatisfiable, so don't try */
+				*qual_ok = false;
+				return;
+			}
+
+			/* decremented scan key value becomes skip array's new high_value */
+			oldContext = MemoryContextSwitchTo(so->arrayContext);
+			array->sksup.high_elem = _bt_apply_decrement(rel, skey, array);
+			MemoryContextSwitchTo(oldContext);
+			break;
+		case BTLessEqualStrategyNumber:
+			oldContext = MemoryContextSwitchTo(so->arrayContext);
+			array->sksup.high_elem = datumCopy(skey->sk_argument,
+											   attr->attbyval, attr->attlen);
+			MemoryContextSwitchTo(oldContext);
+			break;
+		case BTEqualStrategyNumber:
+			/* _bt_preprocess_array_keys should have avoided this */
+			elog(ERROR, "equality strategy scan key conflicts with skip key for attribute %d on index \"%s\"",
+				 skey->sk_attno, RelationGetRelationName(rel));
+			break;
+		case BTGreaterEqualStrategyNumber:
+			oldContext = MemoryContextSwitchTo(so->arrayContext);
+			array->sksup.low_elem = datumCopy(skey->sk_argument,
+											  attr->attbyval, attr->attlen);
+			MemoryContextSwitchTo(oldContext);
+			break;
+		case BTGreaterStrategyNumber:
+
+			/*
+			 * detect if scan key argument will be > high_value once
+			 * incremented
+			 */
+			cmpresult = _bt_compare_array_skey(orderprocp,
+											   skey->sk_argument, false,
+											   array->sksup.high_elem, false,
+											   arraysk);
+			if (cmpresult >= 0)
+			{
+				/* incrementing would make qual unsatisfiable, so don't try */
+				*qual_ok = false;
+				return;
+			}
+
+			/* incremented scan key value becomes skip array's new low_value */
+			oldContext = MemoryContextSwitchTo(so->arrayContext);
+			array->sksup.low_elem = _bt_apply_increment(rel, skey, array);
+			MemoryContextSwitchTo(oldContext);
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	/*
+	 * Is the qual contradictory, or is it merely "redundant" with consed-up
+	 * skip array?
+	 */
+	cmpresult = _bt_compare_array_skey(orderproc,	/* don't use orderprocp */
+									   array->sksup.low_elem, false,
+									   array->sksup.high_elem, false,
+									   arraysk);
+	*qual_ok = (cmpresult <= 0);
 }
 
 /*
@@ -1130,7 +1709,8 @@ _bt_compare_array_elements(const void *a, const void *b, void *arg)
 static inline int32
 _bt_compare_array_skey(FmgrInfo *orderproc,
 					   Datum tupdatum, bool tupnull,
-					   Datum arrdatum, ScanKey cur)
+					   Datum arrdatum, bool arrnull,
+					   ScanKey cur)
 {
 	int32		result = 0;
 
@@ -1138,14 +1718,14 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 
 	if (tupnull)				/* NULL tupdatum */
 	{
-		if (cur->sk_flags & SK_ISNULL)
+		if (arrnull)
 			result = 0;			/* NULL "=" NULL */
 		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = -1;		/* NULL "<" NOT_NULL */
 		else
 			result = 1;			/* NULL ">" NOT_NULL */
 	}
-	else if (cur->sk_flags & SK_ISNULL) /* NOT_NULL tupdatum, NULL arrdatum */
+	else if (arrnull)			/* NOT_NULL tupdatum, NULL arrdatum */
 	{
 		if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = 1;			/* NOT_NULL ">" NULL */
@@ -1211,6 +1791,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1246,7 +1828,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[low_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result <= 0)
 				{
@@ -1274,7 +1856,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[high_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result >= 0)
 				{
@@ -1301,7 +1883,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 		arrdatum = array->elem_values[mid_elem];
 
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										arrdatum, cur);
+										arrdatum, false, cur);
 
 		if (result == 0)
 		{
@@ -1326,13 +1908,123 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	 */
 	if (low_elem != mid_elem)
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										array->elem_values[low_elem], cur);
+										array->elem_values[low_elem], false,
+										cur);
 
 	*set_elem_result = result;
 
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Skip scan arrays procedurally generate their elements on-demand.  They
+ * largely function in the same way as standard arrays.  They can be rolled
+ * over by standard arrays (standard array can also roll over skip arrays).
+ *
+ * This routine doesn't return an index into the array, because the array
+ * doesn't actually have any elements (it has low_value and high_value, which
+ * indicate the range of values that the array can generate).  Note that this
+ * may include a NULL value/an IS NULL qual (unlike with true arrays).
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this skip
+ * array's scan key.  We can apply this information to find the next matching
+ * array element in the current scan direction using fewer comparisons.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Datum		arrdatum;
+	bool		arrnull;
+
+	Assert(!ScanDirectionIsNoMovement(dir));
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Compare tupdatum against "first array element" in the current scan
+	 * direction first (and allow NULL to be treated as a possible element).
+	 *
+	 * Optimization: don't have to bother with this when passed a skip array
+	 * that is known to have triggered array advancement.
+	 */
+	if (!cur_elem_trig)
+	{
+		if (ScanDirectionIsForward(dir))
+		{
+			arrdatum = array->sksup.low_elem;
+			arrnull = array->null_elem && (cur->sk_flags & SK_BT_NULLS_FIRST);
+		}
+		else
+		{
+			arrdatum = array->sksup.high_elem;
+			arrnull = array->null_elem && !(cur->sk_flags & SK_BT_NULLS_FIRST);
+		}
+
+		*set_elem_result = _bt_compare_array_skey(orderproc,
+												  tupdatum, tupnull,
+												  arrdatum, arrnull,
+												  cur);
+
+		/*
+		 * Optimization: return early when >= lower bound happens to be an
+		 * exact match (or when <= upper bound is an exact match during a
+		 * backwards scan)
+		 */
+		if (*set_elem_result == 0)
+			return;
+
+		/* Is tupdatum "before the start" of our lowest "element"? */
+		if ((ScanDirectionIsForward(dir) && *set_elem_result < 0) ||
+			(ScanDirectionIsBackward(dir) && *set_elem_result > 0))
+			return;
+	}
+
+	/*
+	 * Now compare tupdatum to the "last array element" in the current scan
+	 * direction (and allow NULL to be treated as a possible element)
+	 */
+	if (ScanDirectionIsForward(dir))
+	{
+		arrdatum = array->sksup.high_elem;
+		arrnull = array->null_elem && !(cur->sk_flags & SK_BT_NULLS_FIRST);
+	}
+	else
+	{
+		arrdatum = array->sksup.low_elem;
+		arrnull = array->null_elem && (cur->sk_flags & SK_BT_NULLS_FIRST);
+	}
+
+	*set_elem_result = _bt_compare_array_skey(orderproc,
+											  tupdatum, tupnull,
+											  arrdatum, arrnull,
+											  cur);
+
+	/* Is tupdatum "after the end" of our highest "element"? */
+	if ((ScanDirectionIsForward(dir) && *set_elem_result > 0) ||
+		(ScanDirectionIsBackward(dir) && *set_elem_result < 0))
+		return;
+
+	/*
+	 * tupdatum must be within the range of the skip array.  Have our caller
+	 * treat tupdatum as one of the array's elements.
+	 */
+	*set_elem_result = 0;
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1342,29 +2034,257 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
 		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
 		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, curArrayKey,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = false;
 }
 
+/*
+ * _bt_scankey_decrement() -- decrement scan key's sk_argument
+ *
+ * Unsets scan key "IS NULL" flags when required, and handles memory
+ * management for pass-by-reference types.
+ */
+static void
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (skey->sk_flags & SK_ISNULL)
+		_bt_scankey_unset_isnull(rel, skey, array);
+	else
+	{
+		Datum		dec_sk_argument;
+		Form_pg_attribute attr;
+
+		/* Get a decremented copy of existing sk_argument */
+		dec_sk_argument = _bt_apply_decrement(rel, skey, array);
+
+		/* Free memory previously allocated for sk_argument if needed */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+
+		/* Set decremented copy of original sk_argument in scan key */
+		skey->sk_argument = dec_sk_argument;
+	}
+}
+
+/*
+ * _bt_scankey_increment() -- increment scan key's sk_argument
+ *
+ * Unsets scan key "IS NULL" flags when required, and handles memory
+ * management for pass-by-reference types.
+ */
+static void
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (skey->sk_flags & SK_ISNULL)
+		_bt_scankey_unset_isnull(rel, skey, array);
+	else
+	{
+		Datum		inc_sk_argument;
+		Form_pg_attribute attr;
+
+		/* Get an incremented copy of existing sk_argument */
+		inc_sk_argument = _bt_apply_increment(rel, skey, array);
+
+		/* Free memory previously allocated for sk_argument if needed */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+
+		/* Set incremented copy of original sk_argument in scan key */
+		skey->sk_argument = inc_sk_argument;
+	}
+}
+
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Set element to NULL (lowest/highest element) */
+		skey->sk_argument = (Datum) 0;
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else
+	{
+		/* Lowest array element isn't NULL */
+		if (low_not_high)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/*
+	 * Treat tupdatum/tupnull as a matching array element.
+	 *
+	 * We just copy tupdatum into the array's scan key (there is no
+	 * conventional array element for us to set, of course).
+	 */
+	if (tupnull)
+	{
+		/*
+		 * Unlike standard arrays, skip arrays sometimes need to locate NULLs.
+		 * We can treat them as just another value from the domain of indexed
+		 * values.
+		 */
+		Assert(array->null_elem);
+		Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL)));
+
+		skey->sk_argument = (Datum) 0;
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else
+	{
+		skey->sk_argument = datumCopy(tupdatum,
+									  attr->attbyval, attr->attlen);
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	}
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(array->null_elem);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- increment/decrement scan key to NULL
+ *
+ * Sets scan key to "IS NULL", and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & SK_ISNULL));
+	Assert(array->null_elem);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1380,6 +2300,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1391,10 +2312,24 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	{
 		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
 		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		FmgrInfo   *orderproc = &so->orderProcs[curArrayKey->scan_key];
 		int			cur_elem = curArrayKey->cur_elem;
 		int			num_elems = curArrayKey->num_elems;
 		bool		rolled = false;
 
+		/* Handle incrementing a skip array */
+		if (num_elems == -1)
+		{
+			/* Attempt to incrementally advance this skip scan array */
+			if (_bt_advance_skip_array_key_increment(rel, dir, curArrayKey,
+													 skey, orderproc))
+				return true;
+
+			/* Array rolled over.  Need to advance next array key, if any. */
+			continue;
+		}
+
+		/* Handle incrementing a true array */
 		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
 		{
 			cur_elem = 0;
@@ -1411,7 +2346,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 		if (!rolled)
 			return true;
 
-		/* Need to advance next array key, if any */
+		/* Array rolled over.  Need to advance next array key, if any. */
 	}
 
 	/*
@@ -1429,6 +2364,95 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	return false;
 }
 
+/*
+ * _bt_advance_skip_array_key_increment() -- increment a skip scan array
+ *
+ * Returns true when the skip array was successfully incremented to the next
+ * value in the current scan direction, dir.  Otherwise handles roll over by
+ * setting array to its final element for the current scan direction.
+ */
+static bool
+_bt_advance_skip_array_key_increment(Relation rel, ScanDirection dir,
+									 BTArrayKeyInfo *array, ScanKey skey,
+									 FmgrInfo *orderproc)
+{
+	Datum		sk_argument = skey->sk_argument;
+	bool		sk_isnull = (skey->sk_flags & SK_ISNULL) != 0;
+	int			compare;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->num_elems == -1);
+
+	if (ScanDirectionIsForward(dir))
+	{
+		/* high_elem is final non-NULL element in current scan direction */
+		compare = _bt_compare_array_skey(orderproc,
+										 array->sksup.high_elem, false,
+										 sk_argument, sk_isnull,
+										 skey);
+		if (compare > 0)
+		{
+			/* Increment non-NULL element to next non-NULL element */
+			_bt_scankey_increment(rel, skey, array);
+
+			return true;
+		}
+		else if (compare == 0 && array->null_elem &&
+				 !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/*
+			 * Existing sk_argument was already equal to high_elem.  Increment
+			 * from high_elem to final NULL element (without calling opclass
+			 * support function, which doesn't know how to handle NULLs).
+			 */
+			_bt_scankey_set_isnull(rel, skey, array);
+
+			return true;
+		}
+
+		/* Exhausted all array elements in current scan direction */
+	}
+	else
+	{
+		/* low_elem is final non-NULL element in current scan direction */
+		compare = _bt_compare_array_skey(orderproc,
+										 array->sksup.low_elem, false,
+										 sk_argument, sk_isnull,
+										 skey);
+		if (compare < 0)
+		{
+			/* Decrement non-NULL element to previous non-NULL element */
+			_bt_scankey_decrement(rel, skey, array);
+
+			return true;
+		}
+		else if (compare == 0 && array->null_elem &&
+				 (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/*
+			 * Existing sk_argument was already equal to low_elem.  Decrement
+			 * from low_elem to final NULL element (without calling opclass
+			 * support function, which doesn't know how to handle NULLs).
+			 */
+			_bt_scankey_set_isnull(rel, skey, array);
+
+			return true;
+		}
+
+		/* Exhausted all array elements in current scan direction */
+	}
+
+	/*
+	 * Skip array rolls over.  Start over at the array's lowest sorting value
+	 * (or its highest value, for backward scans).
+	 */
+	_bt_scankey_set_low_or_high(rel, skey, array, ScanDirectionIsForward(dir));
+
+	/* Caller must consider earlier/more significant arrays in turn */
+	return false;
+}
+
 /*
  * _bt_rewind_nonrequired_arrays() -- Rewind non-required arrays
  *
@@ -1466,6 +2490,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1473,7 +2498,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1485,16 +2509,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No skipping of non-required arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1558,6 +2576,8 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 	for (int ikey = sktrig; ikey < so->numberOfKeys; ikey++)
 	{
 		ScanKey		cur = so->keyData + ikey;
+		Datum		sk_argument = cur->sk_argument;
+		bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
 		Datum		tupdatum;
 		bool		tupnull;
 		int32		result;
@@ -1621,7 +2641,8 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		result = _bt_compare_array_skey(&so->orderProcs[ikey],
 										tupdatum, tupnull,
-										cur->sk_argument, cur);
+										sk_argument, sk_isnull,
+										cur);
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1954,18 +2975,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1990,18 +3002,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2019,15 +3022,27 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * Skip array.  "Binary search" by checking if tupdatum/tupnull
+			 * are within the low_value/high_value range of the skip array.
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
+			Datum		sk_argument = cur->sk_argument;
+			bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
+
 			Assert(sktrig_required && required);
 
 			/*
@@ -2041,7 +3056,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 */
 			result = _bt_compare_array_skey(&so->orderProcs[ikey],
 											tupdatum, tupnull,
-											cur->sk_argument, cur);
+											sk_argument, sk_isnull, cur);
 		}
 
 		/*
@@ -2061,6 +3076,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * its final element is.  Once outside the loop we'll then "increment
 		 * this array's set_elem" by calling _bt_advance_array_keys_increment.
 		 * That way the process rolls over to higher order arrays as needed.
+		 * The skip array case will set the array's scan key to the final
+		 * valid element for the current scan direction, which is equivalent
+		 * (when we have a real set_elem "match" it's just the final element
+		 * in the current scan direction).
 		 *
 		 * Under this scheme any required arrays only ever ratchet forwards
 		 * (or backwards), and always do so to the maximum possible extent
@@ -2100,11 +3119,62 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+
+		if (!array)
+			continue;			/* no element to set in non-array */
+
+		/* Conventional arrays have a valid set_elem for us to advance to */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * Conceptually, skip arrays also have array elements.  The actual
+		 * elements/values are generated procedurally and on demand.
+		 */
+		Assert(cur->sk_flags & SK_BT_SKIP);
+		Assert(array->num_elems == -1);
+		Assert(required);
+
+		if (result == 0)
+		{
+			/*
+			 * Anything within the range of possible element values is treated
+			 * as "a match for one of the array's elements".  Store the next
+			 * scan key argument value by taking a copy of the tupdatum value
+			 * from caller's tuple (or set scan key IS NULL when tupnull, iff
+			 * the array's range of possible elements covers NULL).
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
+		}
+		else if (beyond_end_advance)
+		{
+			/*
+			 * We need to set the array element to the final "element" in the
+			 * current scan direction for "beyond end of array element" array
+			 * advancement.  See above for an explanation.
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else
+		{
+			/*
+			 * The closest matching element is the lowest element; even that
+			 * still puts us ahead of caller's tuple in the key space.  This
+			 * process has to carry to any lower-order arrays.  See above for
+			 * an explanation.
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
 		}
 	}
 
@@ -2460,10 +3530,12 @@ end_toplevel_scan:
 /*
  *	_bt_preprocess_keys() -- Preprocess scan keys
  *
+ * The first call here (per btrescan) allocates so->keyData[].
  * The given search-type keys (taken from scan->keyData[])
  * are copied to so->keyData[] with possible transformation.
  * scan->numberOfKeys is the number of input keys, so->numberOfKeys gets
- * the number of output keys (possibly less, never greater).
+ * the number of output keys.  Calling here a second time (during the same
+ * btrescan) is a no-op.
  *
  * The output keys are marked with additional sk_flags bits beyond the
  * system-standard bits supplied by the caller.  The DESC and NULLS_FIRST
@@ -2483,6 +3555,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also cons up skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2550,9 +3624,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	int16	   *indoption = scan->indexRelation->rd_indoption;
 	int			new_numberOfKeys;
 	int			numberOfEqualCols;
-	ScanKey		inkeys;
-	ScanKey		outkeys;
-	ScanKey		cur;
+	ScanKey		inputsk;
 	BTScanKeyPreproc xform[BTMaxStrategyNumber];
 	bool		test_result;
 	int			i,
@@ -2584,7 +3656,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		return;					/* done if qual-less scan */
 
 	/* If any keys are SK_SEARCHARRAY type, set up array-key info */
-	arrayKeyData = _bt_preprocess_array_keys(scan);
+	arrayKeyData = _bt_preprocess_array_keys(scan, &numberOfKeys);
 	if (!so->qual_ok)
 	{
 		/* unmatchable array, so give up */
@@ -2598,32 +3670,36 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	 */
 	if (arrayKeyData)
 	{
-		inkeys = arrayKeyData;
+		inputsk = arrayKeyData;
 
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
 	}
 	else
-		inkeys = scan->keyData;
+		inputsk = scan->keyData;
+
+	/*
+	 * Now that we have an estimate of the number of output scan keys
+	 * (including any skip array scan keys), allocate space for them
+	 */
+	so->keyData = palloc(sizeof(ScanKeyData) * numberOfKeys);
 
-	outkeys = so->keyData;
-	cur = &inkeys[0];
 	/* we check that input keys are correctly ordered */
-	if (cur->sk_attno < 1)
+	if (inputsk->sk_attno < 1)
 		elog(ERROR, "btree index keys must be ordered by attribute");
 
 	/* We can short-circuit most of the work if there's just one key */
 	if (numberOfKeys == 1)
 	{
 		/* Apply indoption to scankey (might change sk_strategy!) */
-		if (!_bt_fix_scankey_strategy(cur, indoption))
+		if (!_bt_fix_scankey_strategy(inputsk, indoption))
 			so->qual_ok = false;
-		memcpy(outkeys, cur, sizeof(ScanKeyData));
+		memcpy(so->keyData, inputsk, sizeof(ScanKeyData));
 		so->numberOfKeys = 1;
 		/* We can mark the qual as required if it's for first index col */
-		if (cur->sk_attno == 1)
-			_bt_mark_scankey_required(outkeys);
+		if (inputsk->sk_attno == 1)
+			_bt_mark_scankey_required(so->keyData);
 		if (arrayKeyData)
 		{
 			/*
@@ -2631,8 +3707,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * (we'll miss out on the single value array transformation, but
 			 * that's not nearly as important when there's only one scan key)
 			 */
-			Assert(cur->sk_flags & SK_SEARCHARRAY);
-			Assert(cur->sk_strategy != BTEqualStrategyNumber ||
+			Assert(so->keyData[0].sk_flags & SK_SEARCHARRAY);
+			Assert(so->keyData[0].sk_strategy != BTEqualStrategyNumber ||
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
@@ -2660,12 +3736,12 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	 * handle after-last-key processing.  Actual exit from the loop is at the
 	 * "break" statement below.
 	 */
-	for (i = 0;; cur++, i++)
+	for (i = 0;; inputsk++, i++)
 	{
 		if (i < numberOfKeys)
 		{
 			/* Apply indoption to scankey (might change sk_strategy!) */
-			if (!_bt_fix_scankey_strategy(cur, indoption))
+			if (!_bt_fix_scankey_strategy(inputsk, indoption))
 			{
 				/* NULL can't be matched, so give up */
 				so->qual_ok = false;
@@ -2677,12 +3753,12 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		 * If we are at the end of the keys for a particular attr, finish up
 		 * processing and emit the cleaned-up keys.
 		 */
-		if (i == numberOfKeys || cur->sk_attno != attno)
+		if (i == numberOfKeys || inputsk->sk_attno != attno)
 		{
 			int			priorNumberOfEqualCols = numberOfEqualCols;
 
 			/* check input keys are correctly ordered */
-			if (i < numberOfKeys && cur->sk_attno < attno)
+			if (i < numberOfKeys && inputsk->sk_attno < attno)
 				elog(ERROR, "btree index keys must be ordered by attribute");
 
 			/*
@@ -2741,7 +3817,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
+						Assert(!array || array->num_elems > 0 ||
+							   array->num_elems == -1);
 						xform[j].skey = NULL;
 						xform[j].ikey = -1;
 					}
@@ -2786,7 +3863,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			}
 
 			/*
-			 * Emit the cleaned-up keys into the outkeys[] array, and then
+			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
 			 */
@@ -2794,7 +3871,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			{
 				if (xform[j].skey)
 				{
-					ScanKey		outkey = &outkeys[new_numberOfKeys++];
+					ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
 					memcpy(outkey, xform[j].skey, sizeof(ScanKeyData));
 					if (arrayKeyData)
@@ -2811,19 +3888,19 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				break;
 
 			/* Re-initialize for new attno */
-			attno = cur->sk_attno;
+			attno = inputsk->sk_attno;
 			memset(xform, 0, sizeof(xform));
 		}
 
 		/* check strategy this key's operator corresponds to */
-		j = cur->sk_strategy - 1;
+		j = inputsk->sk_strategy - 1;
 
 		/* if row comparison, push it directly to the output array */
-		if (cur->sk_flags & SK_ROW_HEADER)
+		if (inputsk->sk_flags & SK_ROW_HEADER)
 		{
-			ScanKey		outkey = &outkeys[new_numberOfKeys++];
+			ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
-			memcpy(outkey, cur, sizeof(ScanKeyData));
+			memcpy(outkey, inputsk, sizeof(ScanKeyData));
 			if (arrayKeyData)
 				keyDataMap[new_numberOfKeys - 1] = i;
 			if (numberOfEqualCols == attno - 1)
@@ -2837,19 +3914,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			continue;
 		}
 
-		/*
-		 * Does this input scan key require further processing as an array?
-		 */
-		if (cur->sk_strategy == InvalidStrategy)
-		{
-			/* _bt_preprocess_array_keys marked this array key redundant */
-			Assert(arrayKeyData);
-			Assert(cur->sk_flags & SK_SEARCHARRAY);
-			continue;
-		}
-
-		if (cur->sk_strategy == BTEqualStrategyNumber &&
-			(cur->sk_flags & SK_SEARCHARRAY))
+		if (inputsk->sk_strategy == BTEqualStrategyNumber &&
+			(inputsk->sk_flags & SK_SEARCHARRAY))
 		{
 			/* _bt_preprocess_array_keys kept this array key */
 			Assert(arrayKeyData);
@@ -2863,7 +3929,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		if (xform[j].skey == NULL)
 		{
 			/* nope, so this scan key wins by default (at least for now) */
-			xform[j].skey = cur;
+			xform[j].skey = inputsk;
 			xform[j].ikey = i;
 			xform[j].arrayidx = arrayidx;
 		}
@@ -2881,7 +3947,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/*
 				 * Have to set up array keys
 				 */
-				if ((cur->sk_flags & SK_SEARCHARRAY))
+				if ((inputsk->sk_flags & SK_SEARCHARRAY))
 				{
 					array = &so->arrayKeys[arrayidx - 1];
 					orderproc = so->orderProcs + i;
@@ -2909,13 +3975,15 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				 */
 			}
 
-			if (_bt_compare_scankey_args(scan, cur, cur, xform[j].skey,
-										 array, orderproc, &test_result))
+			if (_bt_compare_scankey_args(scan, inputsk, inputsk,
+										 xform[j].skey, array, orderproc,
+										 &test_result))
 			{
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
+					Assert(!array || array->num_elems > 0 ||
+						   array->num_elems == -1);
 
 					/*
 					 * New key is more restrictive, and so replaces old key...
@@ -2923,7 +3991,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 					if (j != (BTEqualStrategyNumber - 1) ||
 						!(xform[j].skey->sk_flags & SK_SEARCHARRAY))
 					{
-						xform[j].skey = cur;
+						xform[j].skey = inputsk;
 						xform[j].ikey = i;
 						xform[j].arrayidx = arrayidx;
 					}
@@ -2936,7 +4004,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 						 * scan key.  _bt_compare_scankey_args expects us to
 						 * always keep arrays (and discard non-arrays).
 						 */
-						Assert(!(cur->sk_flags & SK_SEARCHARRAY));
+						Assert(!(inputsk->sk_flags & SK_SEARCHARRAY));
 					}
 				}
 				else if (j == (BTEqualStrategyNumber - 1))
@@ -2959,14 +4027,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				 * even with incomplete opfamilies.  _bt_advance_array_keys
 				 * depends on this.
 				 */
-				ScanKey		outkey = &outkeys[new_numberOfKeys++];
+				ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
 				memcpy(outkey, xform[j].skey, sizeof(ScanKeyData));
 				if (arrayKeyData)
 					keyDataMap[new_numberOfKeys - 1] = xform[j].ikey;
 				if (numberOfEqualCols == attno - 1)
 					_bt_mark_scankey_required(outkey);
-				xform[j].skey = cur;
+				xform[j].skey = inputsk;
 				xform[j].ikey = i;
 				xform[j].arrayidx = arrayidx;
 			}
@@ -3057,10 +4125,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3135,6 +4204,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Don't allow skip array to generate IS NULL scan key/element */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3208,6 +4293,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3380,13 +4466,6 @@ _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption)
 		return true;
 	}
 
-	if (skey->sk_strategy == InvalidStrategy)
-	{
-		/* Already-eliminated array scan key; don't need to fix anything */
-		Assert(skey->sk_flags & SK_SEARCHARRAY);
-		return true;
-	}
-
 	/* Adjust strategy for DESC, if we didn't already */
 	if ((addflags & SK_BT_DESC) && !(skey->sk_flags & SK_BT_DESC))
 		skey->sk_strategy = BTCommuteStrategyNumber(skey->sk_strategy);
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index edb09d4e3..e945686c8 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -96,6 +96,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 9c854e0e5..ea3d0f4b5 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,39 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	Assert(dexisting > DATEVAL_NOBEGIN);
+
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	Assert(dexisting < DATEVAL_NOEND);
+
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 date_finite(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index 8c6fc80c3..91682edd5 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -83,6 +83,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 5f5d7959d..c1df7be9f 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -6800,6 +6800,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		found_skip;
 	bool		found_saop;
 	bool		found_is_null_op;
 	double		num_sa_scans;
@@ -6825,6 +6826,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	found_skip = false;
 	found_saop = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
@@ -6833,15 +6835,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
 		ListCell   *lc2;
 
+		/*
+		 * XXX For now we just cost skip scans via generic rules: make a
+		 * uniform assumption that there will be 10 primitive index scans per
+		 * skipped attribute, relying on the "1/3 of all index pages" cap that
+		 * this costing has used since Postgres 17.  Also assume that skipping
+		 * won't take place for an index that has fewer than 100 pages.
+		 *
+		 * The current approach to costing leaves much to be desired, but is
+		 * at least better than nothing at all (keeping the code as it is on
+		 * HEAD just makes testing and review inconvenient).
+		 */
 		if (indexcol != iclause->indexcol)
 		{
 			/* Beginning of a new column's quals */
 			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			{
+				found_skip = true;	/* skip when no '=' qual for indexcol */
+				if (index->pages < 100)
+					break;
+				num_sa_scans += 10;
+			}
 			eqQualHere = false;
 			indexcol++;
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			{
+				/* no quals at all for indexcol */
+				found_skip = true;
+				if (index->pages < 100)
+					break;
+				num_sa_scans += 10 * (indexcol - iclause->indexcol);
+				continue;
+			}
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6914,6 +6939,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..9665e4985
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,54 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scans.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include <limits.h>
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		Datum		low_elem = sksup->low_elem;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->high_elem = low_elem;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 45eb1b2fe..a9222f896 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -390,6 +393,68 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	Assert(false);
+
+	return UUIDPGetDatum(uuid);
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	Assert(false);
+
+	return UUIDPGetDatum(uuid);
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 6f4188599..8ec3f150a 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -3523,6 +3524,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..9662fb2ba 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -583,6 +583,19 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure via an index <quote>skip scan</quote>.  The
+     APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 22d8ad1aa..f17dd3456 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1056,7 +1063,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal);
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1069,7 +1077,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal);
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1082,7 +1091,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal);
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..8b6b775c1 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 3bbe4c5f9..a8d5be6c1 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5138,9 +5138,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..4246afefd 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e6c1caf64..369eca3a4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2650,6 +2651,8 @@ SingleBoundSortItem
 SinglePartitionSpec
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

In reply to: Peter Geoghegan (#5)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Jul 2, 2024 at 12:25 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached v2 fixes this bug. The problem was that the skip support
function used by the "char" opclass assumed signed char comparisons,
even though the authoritative B-Tree comparator (support function 1)
uses signed comparisons (via uint8 casting). A simple oversight.

Although v2 gives correct answers to the queries, the scan itself
performs an excessive amount of leaf page accesses. In short, it
behaves just like a full index scan would, even though we should
expect it to skip over significant runs of the index. So that's
another bug.

It looks like the queries you posted have a kind of adversarial
quality to them, as if they were designed to confuse the
implementation. Was it intentional? Did you take them from an existing
test suite somewhere?

The custom instrumentation I use to debug these issues shows:

_bt_readpage: 🍀 1981 with 175 offsets/tuples (leftsib 4032, rightsib 3991) ➡️
_bt_readpage first: (c, n)=(b, 998982285), TID='(1236,173)',
0x7f1464fe9fc0, from non-pivot offnum 2 started page
_bt_readpage final: , (nil), continuescan high key check did not set
so->currPos.moreRight=false ➡️ 🟢
_bt_readpage stats: currPos.firstItem: 0, currPos.lastItem: 173,
nmatching: 174 ✅
_bt_readpage: 🍀 3991 with 175 offsets/tuples (leftsib 1981, rightsib 9) ➡️
_bt_readpage first: (c, n)=(b, 999474517), TID='(4210,9)',
0x7f1464febfc8, from non-pivot offnum 2 started page
_bt_readpage final: , (nil), continuescan high key check did not set
so->currPos.moreRight=false ➡️ 🟢
_bt_readpage stats: currPos.firstItem: 0, currPos.lastItem: 173,
nmatching: 174 ✅
_bt_readpage: 🍀 9 with 229 offsets/tuples (leftsib 3991, rightsib 3104) ➡️
_bt_readpage first: (c, n)=(c, 1606), TID='(882,68)', 0x7f1464fedfc0,
from non-pivot offnum 2 started page
_bt_readpage final: , (nil), continuescan high key check did not set
so->currPos.moreRight=false ➡️ 🟢
_bt_readpage stats: currPos.firstItem: 0, currPos.lastItem: -1, nmatching: 0 ❌
_bt_readpage: 🍀 3104 with 258 offsets/tuples (leftsib 9, rightsib 1685) ➡️
_bt_readpage first: (c, n)=(c, 706836), TID='(3213,4)',
0x7f1464feffc0, from non-pivot offnum 2 started page
_bt_readpage final: , (nil), continuescan high key check did not set
so->currPos.moreRight=false ➡️ 🟢
_bt_readpage stats: currPos.firstItem: 0, currPos.lastItem: -1, nmatching: 0 ❌
*** SNIP, many more "nmatching: 0" pages appear after these two ***

The final _bt_advance_array_keys call for leaf page 3991 should be
scheduling a new primitive index scan (i.e. skipping), but that never
happens. Not entirely sure why that is, but it probably has something
to do with _bt_advance_array_keys failing to hit the
"has_required_opposite_direction_only" path for determining if another
primitive scan is required. You're using an inequality required in the
opposite-to-scan-direction here, so that path is likely to be
relevant.

--
Peter Geoghegan

In reply to: Peter Geoghegan (#6)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Jul 2, 2024 at 12:55 PM Peter Geoghegan <pg@bowt.ie> wrote:

Although v2 gives correct answers to the queries, the scan itself
performs an excessive amount of leaf page accesses. In short, it
behaves just like a full index scan would, even though we should
expect it to skip over significant runs of the index. So that's
another bug.

Hit "send" too soon. I simply forgot to run "alter table test1 alter
column c type "char";" before running the query. So, I was mistaken
about there still being a bug in v2. The issue here is that we don't
have support for the underlying type, char(1) -- nothing more.

v2 of the patch with your query 1 (when changed to use the "char"
type/opclass instead of the currently unsupported char(1)
type/opclass) performs 395 index related buffer hits, and 5406 heap
block accesses. Whereas it's 3833 index buffer hits with master
(naturally, the same 5406 heap accesses are required with master). In
short, this query isn't particularly sympathetic to the patch. Nor is
it unsympathetic.

--
Peter Geoghegan

#8Aleksander Alekseev
aleksander@timescale.com
In reply to: Peter Geoghegan (#7)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Hi Peter,

It looks like the queries you posted have a kind of adversarial
quality to them, as if they were designed to confuse the
implementation. Was it intentional?

To some extent. I merely wrote several queries that I would expect
should benefit from skip scans. Since I didn't look at the queries you
used there was a chance that I will hit something interesting.

Attached v2 fixes this bug. The problem was that the skip support
function used by the "char" opclass assumed signed char comparisons,
even though the authoritative B-Tree comparator (support function 1)
uses signed comparisons (via uint8 casting). A simple oversight. Your
test cases will work with this v2, provided you use "char" (instead of
unadorned char) in the create table statements.

Thanks for v2.

If you change your table definition to CREATE TABLE test1(c "char", n
bigint), then your example queries can use the optimization. This
makes a huge difference.

You are right, it does.

Test1 takes 33.7 ms now (53 ms before the path, x1.57)

Test3 I showed before contained an error in the table definition
(Postgres can't do `n bigint, s text DEFAULT 'text_value' || n`). Here
is the corrected test:

```
CREATE TABLE test3(c "char", n bigint, s text);
CREATE INDEX test3_idx ON test3 USING btree(c,n) INCLUDE(s);

INSERT INTO test3
SELECT chr(ascii('a') + random(0,2)) AS c,
random(0, 1_000_000_000) AS n,
'text_value_' || random(0, 1_000_000_000) AS s
FROM generate_series(0, 1_000_000);

EXPLAIN ANALYZE SELECT s FROM test3 WHERE n < 10_000;
```

It runs fast (< 1 ms) and uses the index, as expected.

Test2 with "char" doesn't seem to benefit from the patch anymore
(pretty sure it did in v1). It always chooses Parallel Seq Scans even
if I change the condition to `WHERE n > 999_995_000` or `WHERE n =
999_997_362`. Is it an expected behavior?

I also tried Test4 and Test5.

In Test4 I was curious if scip scans work properly with functional indexes:

```
CREATE TABLE test4(d date, n bigint);
CREATE INDEX test4_idx ON test4 USING btree(extract(year from d),n);

INSERT INTO test4
SELECT ('2024-' || random(1,12) || '-' || random(1,28)) :: date AS d,
random(0, 1_000_000_000) AS n
FROM generate_series(0, 1_000_000);

EXPLAIN ANALYZE SELECT COUNT(*) FROM test4 WHERE n > 900_000_000;
```

The query uses Index Scan, however the performance is worse than with
Seq Scan chosen before the patch. It doesn't matter if I choose '>' or
'=' condition.

Test5 checks how skip scans work with partial indexes:

```
CREATE TABLE test5(c "char", n bigint);
CREATE INDEX test5_idx ON test5 USING btree(c, n) WHERE n > 900_000_000;

INSERT INTO test5
SELECT chr(ascii('a') + random(0,2)) AS c,
random(0, 1_000_000_000) AS n
FROM generate_series(0, 1_000_000);

EXPLAIN ANALYZE SELECT COUNT(*) FROM test5 WHERE n > 950_000_000;
```

It runs fast and choses Index Only Scan. But then I discovered that
without the patch Postgres also uses Index Only Scan for this query. I
didn't know it could do this - what is the name of this technique? The
query takes 17.6 ms with the patch, 21 ms without the patch. Not a
huge win but still.

That's all I have for now.

--
Best regards,
Aleksander Alekseev

In reply to: Aleksander Alekseev (#8)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Jul 5, 2024 at 7:04 AM Aleksander Alekseev
<aleksander@timescale.com> wrote:

Test2 with "char" doesn't seem to benefit from the patch anymore
(pretty sure it did in v1). It always chooses Parallel Seq Scans even
if I change the condition to `WHERE n > 999_995_000` or `WHERE n =
999_997_362`. Is it an expected behavior?

The "char" opclass's skip support routine was totally broken in v1, so
its performance isn't really relevant. In any case v2 didn't make any
changes to the costing, so I'd expect it to use exactly the same query
plan as v1.

The query uses Index Scan, however the performance is worse than with
Seq Scan chosen before the patch. It doesn't matter if I choose '>' or
'=' condition.

That's because the index has a leading/skipped column of type
"numeric", which isn't a supported type just yet (a supported B-Tree
opclass, actually).

The optimization is effective if you create the expression index with
a cast to integer:

CREATE INDEX test4_idx ON test4 USING btree(((extract(year from d))::int4),n);

This performs much better. Now I see "DEBUG: skipping 1 index
attributes" when I run the query "EXPLAIN (ANALYZE, BUFFERS) SELECT
COUNT(*) FROM test4 WHERE n > 900_000_000", which indicates that the
optimization has in fact been used as expected. There are far fewer
buffers hit with this version of your test4, which also indicates that
the optimization has been effective.

Note that the original numeric expression index test4 showed "DEBUG:
skipping 0 index attributes" when the test query ran, which indicated
that the optimization couldn't be used. I suggest that you look out
for that, by running "set client_min_messages to debug2;" from psql
when testing the patch.

It runs fast and choses Index Only Scan. But then I discovered that
without the patch Postgres also uses Index Only Scan for this query. I
didn't know it could do this - what is the name of this technique?

It is a full index scan. These have been possible for many years now
(possibly well over 20 years).

Arguably, the numeric case that didn't use the optimization (your
test4) should have been costed as a full index scan, but it wasn't --
that's why you didn't get a faster sequential scan, which would have
made a little bit more sense. In general, the costing changes in the
patch are very rough.

That said, this particular problem (the test4 numeric issue) should be
fixed by inventing a way for nbtree to use skip scan with types that
lack skip support. It's not primarily a problem with the costing. At
least not in my mind.

--
Peter Geoghegan

In reply to: Peter Geoghegan (#9)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Jul 5, 2024 at 8:44 PM Peter Geoghegan <pg@bowt.ie> wrote:

CREATE INDEX test4_idx ON test4 USING btree(((extract(year from d))::int4),n);

This performs much better. Now I see "DEBUG: skipping 1 index
attributes" when I run the query "EXPLAIN (ANALYZE, BUFFERS) SELECT
COUNT(*) FROM test4 WHERE n > 900_000_000", which indicates that the
optimization has in fact been used as expected. There are far fewer
buffers hit with this version of your test4, which also indicates that
the optimization has been effective.

Actually, with an index-only scan it is 281 buffer hits (including
some small number of VM buffer hits) with the patch, versus 2736
buffer hits on master. So a big change to the number of index page
accesses only.

If you use a plain index scan for this, then the cost of random heap
accesses totally dominates, so skip scan cannot possibly give much
benefit. Even a similar bitmap scan requires 4425 distinct heap page accesses,
which is significantly more than the total number of index pages in
the index. 4425 heap pages is almost the entire table; the table
consists of 4480 mainfork blocks.

This is a very nonselective query. It's not at all surprising that
this query (and others like it) hardly benefit at all, except when we
can use an index-only scan (so that the cost of heap accesses doesn't
totally dominate).

--
Peter Geoghegan

#11Noname
Masahiro.Ikeda@nttdata.com
In reply to: Peter Geoghegan (#1)
2 attachment(s)
RE: Adding skip scan (including MDAM style range skip scan) to nbtree

Hi,

Since I'd like to understand the skip scan to improve the EXPLAIN output
for multicolumn B-Tree Index[1]Improve EXPLAIN output for multicolumn B-Tree Index /messages/by-id/TYWPR01MB1098260B694D27758FE2BA46FB1C92@TYWPR01MB10982.jpnprd01.prod.outlook.com, I began to try the skip scan with some
queries and look into the source code.

I have some feedback and comments.

(1)

At first, I was surprised to look at your benchmark result because the skip scan
index can improve much performance. I agree that there are many users to be
happy with the feature for especially OLAP use-case. I expected to use v18.

(2)

I found the cost is estimated to much higher if the number of skipped attributes
is more than two. Is it expected behavior?

# Test result. The attached file is the detail of tests.

-- Index Scan
-- The actual time is low since the skip scan works well
-- But the cost is higher than one of seqscan
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT * FROM test WHERE id3 = 101;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------
Index Scan using idx_id1_id2_id3 on public.test (cost=0.42..26562.77 rows=984 width=20) (actual time=0.051..15.533 rows=991 loops=1)
Output: id1, id2, id3, value
Index Cond: (test.id3 = 101)
Buffers: shared hit=4402
Planning:
Buffers: shared hit=7
Planning Time: 0.234 ms
Execution Time: 15.711 ms
(8 rows)

-- Seq Scan
-- actual time is high, but the cost is lower than one of the above Index Scan.
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT * FROM test WHERE id3 = 101;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..12676.73 rows=984 width=20) (actual time=0.856..113.861 rows=991 loops=1)
Output: id1, id2, id3, value
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=6370
-> Parallel Seq Scan on public.test (cost=0.00..11578.33 rows=410 width=20) (actual time=0.061..102.016 rows=330 loops=3)
Output: id1, id2, id3, value
Filter: (test.id3 = 101)
Rows Removed by Filter: 333003
Buffers: shared hit=6370
Worker 0: actual time=0.099..98.014 rows=315 loops=1
Buffers: shared hit=2066
Worker 1: actual time=0.054..97.162 rows=299 loops=1
Buffers: shared hit=1858
Planning:
Buffers: shared hit=19
Planning Time: 0.194 ms
Execution Time: 114.129 ms
(18 rows)

I look at btcostestimate() to find the reason and found the bound quals
and cost.num_sa_scans are different from my expectation.

My assumption is
* bound quals is id3=XXX (and id1 and id2 are skipped attributes)
* cost.num_sa_scans = 100 (=10*10 because assuming 10 primitive index scans
per skipped attribute)

But it's wrong. The above index scan result is
* bound quals is NULL
* cost.num_sa_scans = 1

As I know you said the below, but I'd like to know the above is expected or not.

That approach seems far more practicable than preempting the problem
during planning or during nbtree preprocessing. It seems like it'd be
very hard to model the costs statistically. We need revisions to
btcostestimate, of course, but the less we can rely on btcostestimate
the better. As I said, there are no new index paths generated by the
optimizer for any of this.

I couldn't understand why there is the below logic well.

btcostestimate()
(...omit...)
if (indexcol != iclause->indexcol)
{
/* no quals at all for indexcol */
found_skip = true;
if (index->pages < 100)
break;
num_sa_scans += 10 * (indexcol - iclause->indexcol); // why add minus value?
continue; // why skip to add bound quals?
}

(3)

Currently, there is an assumption that "there will be 10 primitive index scans
per skipped attribute". Is any chance to use pg_stats.n_distinct?

[1]: Improve EXPLAIN output for multicolumn B-Tree Index /messages/by-id/TYWPR01MB1098260B694D27758FE2BA46FB1C92@TYWPR01MB10982.jpnprd01.prod.outlook.com
/messages/by-id/TYWPR01MB1098260B694D27758FE2BA46FB1C92@TYWPR01MB10982.jpnprd01.prod.outlook.com

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

Attachments:

compare_cost.sqlapplication/octet-stream; name=compare_cost.sqlDownload
compare_cost_with_v2_patch.outapplication/octet-stream; name=compare_cost_with_v2_patch.outDownload
In reply to: Noname (#11)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Jul 12, 2024 at 1:19 AM <Masahiro.Ikeda@nttdata.com> wrote:

Since I'd like to understand the skip scan to improve the EXPLAIN output
for multicolumn B-Tree Index[1], I began to try the skip scan with some
queries and look into the source code.

Thanks for the review!

Attached is v3, which generalizes skip scan, allowing it to work with
opclasses/types that lack a skip support routine. In other words, v3
makes skip scan work for all types, including continuous types, where
it's impractical or infeasible to add skip support. So now important
types like text and numeric also get the skip scan optimization (it's
not just discrete types like integer and date, as in previous
versions).

I feel very strongly that everything should be implemented as part of
the new skip array abstraction; the patch should only add the concept
of skip arrays, which should work just like SAOP arrays. We should
avoid introducing any special cases. In short, _bt_advance_array_keys
should work in exactly the same way as it does as of Postgres 17
(except for a few representational differences for skip arrays). This
seems essential because _bt_advance_array_keys inherently need to be
able to trigger moving on to the next skip array value when it reaches
the end of a SAOP array (and vice-versa). And so it just makes sense
to abstract-away the differences, hiding the difference in lower level
code.

I have described the new _bt_first behavior that is now available in
this new v3 of the patch as "adding explicit next key probes". While
v3 does make new changes to _bt_first, it's not really a special kind
of index probe. v3 invents new sentinel values instead.

The use of sentinels avoids inventing true special cases: the values
-inf, +inf, as well as variants of = that use a real datum value, but
match on the next key in the index. These new = variants can be
thought of as "+infinitesimal" values. So when _bt_advance_array_keys
has to "increment" the numeric value 5.0, it sets the scan key to the
value "5.0 +infinitesimal". There can never be any matching tuples in
the index (just like with -inf sentinel values), but that doesn't
matter. So the changes v3 makes to _bt_first doesn't change the basic
conceptual model. The added complexity is kept to a manageable level,
particularly within _bt_advance_array_keys, which is already very
complicated.

To help with testing and review, I've added another temporary testing
GUC to v3: skipscan_skipsupport_enabled. This can be set to "false" to
avoid using skip support, even where available. The GUC makes it easy
to measure how skip support routines can help performance (with
discrete types like integer and date).

I found the cost is estimated to much higher if the number of skipped attributes
is more than two. Is it expected behavior?

Yes and no.

Honestly, the current costing is just placeholder code. It is totally
inadequate. I'm not surprised that you found problems with it. I just
didn't put much work into it, because I didn't really know what to do.

# Test result. The attached file is the detail of tests.

-- Index Scan
-- The actual time is low since the skip scan works well
-- But the cost is higher than one of seqscan
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT * FROM test WHERE id3 = 101;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------
Index Scan using idx_id1_id2_id3 on public.test (cost=0.42..26562.77 rows=984 width=20) (actual time=0.051..15.533 rows=991 loops=1)
Output: id1, id2, id3, value
Index Cond: (test.id3 = 101)
Buffers: shared hit=4402
Planning:
Buffers: shared hit=7
Planning Time: 0.234 ms
Execution Time: 15.711 ms
(8 rows)

This is a useful example, because it shows the difficulty with the
costing. I ran this query using my own custom instrumentation of the
scan. I saw that we only ever manage to skip ahead by perhaps 3 leaf
pages at a time, but we still come out ahead. As you pointed out, it's
~7.5x faster than the sequential scan, but not very different to the
equivalent full index scan. At least not very different in terms of
leaf page accesses. Why should we win by this much, for what seems
like a marginal case for skip scan?

Even cases where "skipping" doesn't manage to skip any leaf pages can
still benefit from skipping *index tuples* -- there is more than one
kind of skipping to consider. That is, the patch helps a lot with some
(though not all) cases where I didn't really expect that to happen:
the Postgres 17 SAOP tuple skipping code (the code in
_bt_checkkeys_look_ahead, and the related code in _bt_readpage) helps
quite a bit in "marginal" skip scan cases, even though it wasn't
really designed for that purpose (it was added to avoid regressions in
SAOP array scans for the Postgres 17 work).

I find that some queries using my original example test case are about
twice as fast as an equivalent full index scan, even when only the
fourth and final index column is used in the query predicate. The scan
can't even skip a single leaf page at a time, and yet we still win by
a nice amount. We win, though it is almost by mistake!

This is mostly a good thing. Both for the obvious reason (fast is
better than slow), and because it justifies being so aggressive in
assuming that skip scan might work out during planning (being wrong
without really losing is nice). But there is also a downside: it makes
it even harder to model costs at runtime, from within the optimizer.

If I measure the actual runtime costs other than runtime (e.g.,
buffers accesses), I'm not sure that the optimizer is wrong to think
that the parallel sequential scan is faster. It looks approximately
correct. It is only when we look at runtime that the optimizer's
choice looks wrong. Which is...awkward.

In general, I have very little idea about how to improve the costing
within btcostestimate. I am hoping that somebody has better ideas
about it. btcostestimate is definitely the area where the patch is
weakest right now.

I look at btcostestimate() to find the reason and found the bound quals
and cost.num_sa_scans are different from my expectation.

My assumption is
* bound quals is id3=XXX (and id1 and id2 are skipped attributes)
* cost.num_sa_scans = 100 (=10*10 because assuming 10 primitive index scans
per skipped attribute)

But it's wrong. The above index scan result is
* bound quals is NULL
* cost.num_sa_scans = 1

The logic with cost.num_sa_scans was definitely not what I intended.
That's fixed in v3, at least. But the code in btcostestimate is still
essentially the same as in earlier versions -- it needs to be
completely redesigned (or, uh, designed for the first time).

As I know you said the below, but I'd like to know the above is expected or not.

Currently, there is an assumption that "there will be 10 primitive index scans
per skipped attribute". Is any chance to use pg_stats.n_distinct?

It probably makes sense to use pg_stats.n_distinct here. But how?

If the problem is that we're too pessimistic, then I think that this
will usually (though not always) make us more pessimistic. Isn't that
the wrong direction to go in? (We're probably also too optimistic in
some cases, but being too pessimistic is a bigger problem in
practice.)

For example, your test case involved 11 distinct values in each
column. The current approach of hard-coding 10 (which is just a
temporary hack) should actually make the scan look a bit cheaper than
it would if we used the true ndistinct.

Another underlying problem is that the existing SAOP costing really
isn't very accurate, without skip scan -- that's a big source of the
pessimism with arrays/skipping. Why should we be able to get the true
number of primitive index scans just by multiplying together each
omitted prefix column's ndistinct? That approach is good for getting
the worst case, which is probably relevant -- but it's probably not a
very good assumption for the average case. (Though at least we can cap
the total number of primitive index scans to 1/3 of the total number
of pages in the index in btcostestimate, since we have guarantees
about the worst case as of Postgres 17.)

--
Peter Geoghegan

Attachments:

v3-0001-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v3-0001-Add-skip-scan-to-nbtree.patchDownload
From 35672d7b6a8fa7e78341d7f6580474693a6afd7d Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v3] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on an index (a, b) for queries with a predicate such as "WHERE b = 5".
This is useful in cases where the total number of distinct values in the
column 'a' is reasonably small (think hundreds, possibly thousands).

In effect, a skip scan treats the composite index on (a, b) as if it was
a series of disjunct subindexes -- one subindex per distinct 'a' value.
We exhaustively "search every subindex" using a qual that behaves like
"WHERE a = ANY(<every possible 'a' value>) AND b = 5".

The design of skip scan works by extended the design for arrays
established by commit 5bf748b8.  "Skip arrays" generate their array
values procedurally and on-demand, but otherwise work just like arrays
used by SAOPs.

B-Tree operator classes on discrete types can now optionally provide a
skip support routine.  This is used to generate the next array element
value by incrementing the current value (or by decrementing, in the case
of backwards scans).  When the opclass lacks a skip support routine, we
use sentinel next-key values instead.  Adding skip support makes skip
scans more efficient in cases where there is naturally a good chance
that the very next value will find matching tuples.  For example, during
an index scan with a leading "sales_date" attribute, there is a decent
chance that a scan that just finished returning tuples matching
"sales_date = '2024-06-01' and id = 5000" will find later tuples
matching "sales_date = '2024-06-02' and id = 5000".  It is to our
advantage to skip straight to the relevant "id = 5000" leaf page,
totally avoiding reading earlier "sales_date = '2024-06-02'" leaf pages.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/nbtree.h                 |   24 +-
 src/include/catalog/pg_amproc.dat           |   16 +
 src/include/catalog/pg_proc.dat             |   24 +
 src/include/utils/skipsupport.h             |  124 ++
 src/backend/access/nbtree/nbtcompare.c      |  201 +++
 src/backend/access/nbtree/nbtree.c          |   10 +-
 src/backend/access/nbtree/nbtsearch.c       |  130 +-
 src/backend/access/nbtree/nbtutils.c        | 1666 +++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c     |    4 +
 src/backend/commands/opclasscmds.c          |   25 +
 src/backend/utils/adt/Makefile              |    1 +
 src/backend/utils/adt/date.c                |   34 +
 src/backend/utils/adt/meson.build           |    1 +
 src/backend/utils/adt/selfuncs.c            |   30 +-
 src/backend/utils/adt/skipsupport.c         |   54 +
 src/backend/utils/adt/uuid.c                |   65 +
 src/backend/utils/misc/guc_tables.c         |   23 +
 doc/src/sgml/btree.sgml                     |   13 +
 doc/src/sgml/xindex.sgml                    |   16 +-
 src/test/regress/expected/alter_generic.out |    6 +-
 src/test/regress/expected/psql.out          |    3 +-
 src/test/regress/sql/alter_generic.sql      |    2 +-
 src/tools/pgindent/typedefs.list            |    3 +
 23 files changed, 2293 insertions(+), 182 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 749304334..7cd5902cf 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1032,9 +1034,18 @@ typedef BTScanPosData *BTScanPos;
 typedef struct BTArrayKeyInfo
 {
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* State used by standard arrays that store elements in memory */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* State used by skip arrays, which generate elements procedurally */
+	bool		use_sksup;		/* sksup set to valid routine? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* opclass skip scan support, when use_sksup */
+	ScanKey		low_compare;	/* !use_sksup > and >= lower bound */
+	ScanKey		high_compare; 	/* !use_sksup < and <= upper bound */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1123,6 +1134,11 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* SK_SEARCHARRAY skip scan key */
+#define SK_BT_NEG_INF	0x00080000	/* -inf search SK_SEARCHARRAY */
+#define SK_BT_POS_INF	0x00100000	/* +inf search SK_SEARCHARRAY */
+#define SK_BT_NEXTKEY	0x00200000	/* key after sk_argument */
+#define SK_BT_PREVKEY	0x00400000	/* key before sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1159,6 +1175,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index f639c3a6a..2a8f6f3f1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -122,6 +128,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +149,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +170,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +205,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +275,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 73d9cf858..27921e0df 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2214,6 +2229,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4368,6 +4386,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -9192,6 +9213,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..ab79acb8c
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,124 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scans.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * This happens at the point where the scan determines that another primitive
+ * index scan is required.  The next value is used (in combination with at
+ * least one additional lower-order non-skip key, taken from the SQL query) to
+ * relocate the scan, skipping over many irrelevant leaf pages in the process.
+ *
+ * There are many data types/opclasses where implementing a skip support
+ * scheme is inherently impossible (or at least impractical).  Obviously, it
+ * would be wrong if the "next" value generated by an opclass was actually
+ * after the true next value (any index tuples with the true next value would
+ * be overlooked by the index scan).
+ *
+ * Skip scan generally works best with discrete types such as integer, date,
+ * and boolean: types where there is a decent chance that indexes will contain
+ * contiguous values (in respect of the leading/skipped index attribute).
+ * When gaps/discontinuities are naturally rare (e.g., a leading identity
+ * column in a composite index, a date column preceding a product_id column),
+ * then it makes sense for the skip scan to optimistically assume that the
+ * next distinct indexable value will find directly matching index tuples.
+ * The B-Tree code can fall back on next-key probes for any opclass that
+ * doesn't include a skip support function, but it's a good idea to provide
+ * skip support for types that are likely to see benefits.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called.
+ * If an opclass can only set some of the fields, then it cannot safely
+ * provide a skip support routine (and so must rely on the fallback strategy
+ * used by continuous types, such as numeric).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming standard ascending
+	 * order).  This helps the B-Tree code with finding its initial position
+	 * at the leaf level (during the skip scan's first primitive index scan).
+	 * In other words, it gives the B-Tree code a useful value to start from,
+	 * before any data has been read from the index.
+	 *
+	 * low_elem and high_elem can also be used to prove that a qual is
+	 * unsatisfiable in certain cross-type scenarios.
+	 *
+	 * low_elem and high_elem are also used by skip scans to determine when
+	 * they've reached the final possible value (in the current direction).
+	 * It's typical for the scan to run out of leaf pages before it runs out
+	 * of unscanned indexable values, but it's still useful for the scan to
+	 * have a way to recognize when it has reached the last possible value
+	 * (this saves us a useless probe that just lands on the final leaf page).
+	 *
+	 * Note: the logic for determining that the scan has reached the final
+	 * possible value naturally belongs in the B-Tree code.  The final value
+	 * isn't necessarily the original high_elem/low_elem set by the opclass.
+	 * In particular, it'll be a lower/higher value when B-Tree preprocessing
+	 * determines that the true range of possible values should be restricted,
+	 * due to the presence of an inequality applied to the index's skipped
+	 * attribute.  These are range skip scans.
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (in the case of pass-by-reference
+	 * types).  It's not okay for these functions to leak any memory.
+	 *
+	 * Both decrement and increment callbacks are guaranteed to never be
+	 * called with a NULL "existing" arg.  (In general it is the B-Tree code's
+	 * job to worry about NULLs, and about whether indexed values are stored
+	 * in ASC order or DESC order.)
+	 *
+	 * The decrement callback is guaranteed to only be called with an
+	 * "existing" value that's strictly > the low_elem set by the opclass.
+	 * Similarly, the increment callback is guaranteed to only be called with
+	 * an "existing" value that's strictly < the high_elem set by the opclass.
+	 * Consequently, opclasses don't have to deal with "overflow" themselves
+	 * (though asserting that the B-Tree code got it right is a good idea).
+	 *
+	 * It's quite possible (and very common) for the B-Tree skip scan caller's
+	 * "existing" datum to just be a straight copy of a value that it copied
+	 * from the index.  Operator classes must be liberal in accepting every
+	 * possible representational variation within the underlying data type.
+	 * Opclasses don't have to preserve whatever semantically insignificant
+	 * information the data type might be carrying around, though.
+	 *
+	 * Note: < and > are defined by the opclass's ORDER proc in the usual way.
+	 */
+	Datum		(*decrement) (Relation rel, Datum existing);
+	Datum		(*increment) (Relation rel, Datum existing);
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..48a877613 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,39 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	Assert(bexisting == true);
+
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	Assert(bexisting == false);
+
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +139,39 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	Assert(iexisting > PG_INT16_MIN);
+
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	Assert(iexisting < PG_INT16_MAX);
+
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +195,39 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	Assert(iexisting > PG_INT32_MIN);
+
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	Assert(iexisting < PG_INT32_MAX);
+
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +271,39 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	Assert(iexisting > PG_INT64_MIN);
+
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	Assert(iexisting < PG_INT64_MAX);
+
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +425,39 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	Assert(oexisting > InvalidOid);
+
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	Assert(oexisting < OID_MAX);
+
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +491,38 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	Assert(cexisting > 0);
+
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	Assert(cexisting < UCHAR_MAX);
+
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 686a3206f..9c9cd48f7 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -324,11 +324,8 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	so = (BTScanOpaque) palloc(sizeof(BTScanOpaqueData));
 	BTScanPosInvalidate(so->currPos);
 	BTScanPosInvalidate(so->markPos);
-	if (scan->numberOfKeys > 0)
-		so->keyData = (ScanKey) palloc(scan->numberOfKeys * sizeof(ScanKeyData));
-	else
-		so->keyData = NULL;
 
+	so->keyData = NULL;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->arrayKeys = NULL;
@@ -408,6 +405,11 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 				scan->numberOfKeys * sizeof(ScanKeyData));
 	so->numberOfKeys = 0;		/* until _bt_preprocess_keys sets it */
 	so->numArrayKeys = 0;		/* ditto */
+
+	/* Release private storage allocated in previous btrescan, if any */
+	if (so->keyData != NULL)
+		pfree(so->keyData);
+	so->keyData = NULL;
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 57bcfc7e4..f1bb4e8ee 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -880,7 +880,6 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	Buffer		buf;
 	BTStack		stack;
 	OffsetNumber offnum;
-	StrategyNumber strat;
 	BTScanInsertData inskey;
 	ScanKey		startKeys[INDEX_MAX_KEYS];
 	ScanKeyData notnullkeys[INDEX_MAX_KEYS];
@@ -1022,6 +1021,8 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		ScanKey		chosen;
 		ScanKey		impliesNN;
 		ScanKey		cur;
+		int			ikey = 0,
+					ichosen = 0;
 
 		/*
 		 * chosen is the so-far-chosen key for the current attribute, if any.
@@ -1042,6 +1043,96 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		{
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
+				/*
+				 * Conceptually, skip arrays consist of array elements whose
+				 * values are generated procedurally and on demand.  We need
+				 * special handling for that here.
+				 *
+				 * We must interpret various sentinel values to generate an
+				 * insertion scan key.  This is only actually needed for index
+				 * attributes whose input opclass lacks a skip support routine
+				 * (when skip support is available we'll always be able to
+				 * generate true array element datum values instead).
+				 */
+				if (chosen && chosen->sk_flags & (SK_BT_NEG_INF | SK_BT_POS_INF))
+				{
+					BTArrayKeyInfo *array = NULL;
+
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(!(chosen->sk_flags & (SK_BT_NEXTKEY | SK_BT_PREVKEY)));
+
+					for (; ikey < so->numArrayKeys; ikey++)
+					{
+						array = &so->arrayKeys[ikey];
+						if (array->scan_key == ichosen)
+							break;
+					}
+
+					Assert(array->scan_key == ichosen);
+					Assert(array->num_elems == -1);
+					Assert(!array->use_sksup);
+
+					if (array->null_elem)
+					{
+						/*
+						 * Treat the chosen scan key as having the value -inf
+						 * (or the value +inf, in the backwards scan case) by
+						 * not appending it to the local startKeys[] array.
+						 *
+						 * Note: we expect one or more lower-order required
+						 * keys that won't influence initial positioning (for
+						 * this primitive index scan).  There cannot possibly
+						 * be non-pivot tuples that have values matching -inf,
+						 * though, so this "omission" can have no real impact.
+						 *
+						 * Note: This array has a NULL element, which means
+						 * that there must be no upper/lower inequalities.
+						 * Assert that prepprocessing got this right.
+						 */
+						Assert(!array->low_compare);
+						Assert(!array->high_compare);
+						break;	/* done adding entries to startKeys[] */
+					}
+					else if ((chosen->sk_flags & SK_BT_NEG_INF) &&
+							 array->low_compare)
+					{
+						Assert(ScanDirectionIsForward(dir));
+
+						/* use array's inequality key in startKeys[] */
+						chosen = array->low_compare;
+					}
+					else if ((chosen->sk_flags & SK_BT_POS_INF) &&
+							 array->high_compare)
+					{
+						Assert(ScanDirectionIsBackward(dir));
+
+						/* use array's inequality key in startKeys[] */
+						chosen = array->high_compare;
+					}
+					else
+					{
+						/*
+						 * Array starts at (or ends just before) any non-NULL
+						 * values.  Deduce a NOT NULL key to skip over NULLs.
+						 *
+						 * Note: range skip arrays generated using an explicit
+						 * IS NOT NULL input scan key against an otherwise
+						 * omitted prefix attribute use this path, too.
+						 */
+						impliesNN = chosen;
+						chosen = NULL;
+					}
+
+					/*
+					 * We'll add the chosen inequality (or a deduced NOT NULL
+					 * key) to startKeys[] below.
+					 *
+					 * Note: we usually won't be able to add any additional
+					 * scan keys for index attributes beyond this one.  This
+					 * is okay for the same reason as the -inf/+inf case.
+					 */
+				}
+
 				/*
 				 * Done looking at keys for curattr.  If we didn't find a
 				 * usable boundary key, see if we can deduce a NOT NULL key.
@@ -1075,16 +1166,41 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 				startKeys[keysz++] = chosen;
 
+				/*
+				 * Skip arrays can also use a sk_argument which is marked
+				 * "next key".  This is another sentinel array element value
+				 * requiring special handling here by us.  As with -inf/+inf
+				 * sentinels, there cannot be any exact non-pivot matches.
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXTKEY | SK_BT_PREVKEY))
+				{
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(!(chosen->sk_flags & (SK_BT_NEG_INF | SK_BT_POS_INF)));
+					Assert(chosen->sk_strategy == BTEqualStrategyNumber);
+
+					/*
+					 * Adjust strat_total, so that our = key gets treated like
+					 * a > key (or like a < key).
+					 *
+					 * The key is still conceptually a = key; we only do this
+					 * because there's no explicit next/prev key we can use.
+					 */
+					if (chosen->sk_flags & SK_BT_NEXTKEY)
+						strat_total = BTGreaterStrategyNumber;
+					else
+						strat_total = BTLessStrategyNumber;
+					break;
+				}
+
 				/*
 				 * Adjust strat_total, and quit if we have stored a > or <
 				 * key.
 				 */
-				strat = chosen->sk_strategy;
-				if (strat != BTEqualStrategyNumber)
+				if (chosen->sk_strategy != BTEqualStrategyNumber)
 				{
-					strat_total = strat;
-					if (strat == BTGreaterStrategyNumber ||
-						strat == BTLessStrategyNumber)
+					strat_total = chosen->sk_strategy;
+					if (chosen->sk_strategy == BTGreaterStrategyNumber ||
+						chosen->sk_strategy == BTLessStrategyNumber)
 						break;
 				}
 
@@ -1103,6 +1219,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 				curattr = cur->sk_attno;
 				chosen = NULL;
 				impliesNN = NULL;
+				ichosen = -1;
 			}
 
 			/*
@@ -1127,6 +1244,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 				case BTEqualStrategyNumber:
 					/* override any non-equality choice */
 					chosen = cur;
+					ichosen = i;
 					break;
 				case BTGreaterEqualStrategyNumber:
 				case BTGreaterStrategyNumber:
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index d6de2072d..133cb4687 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,50 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.
+ *
+ * For example, setting skipscan_prefix_cols=1 before an index scan with qual
+ * "WHERE b = 1 AND c > 42" will make us generate a skip scan key on the
+ * column 'a' (which is attnum 1) only, preventing us from adding one for the
+ * column 'c' (and so 'c' will still have an inequality scan key, required in
+ * only one direction -- 'c' won't be output as a "range" skip key/array).
+ *
+ * The same scan keys will be output when skipscan_prefix_cols=2, given the
+ * same query/qual, since we naturally get a required equality scan key on 'b'
+ * from the input scan keys (provided we at least manage to add a skip scan
+ * key on 'a' that "anchors its required-ness" to the 'b' scan key.)
+ *
+ * When skipscan_prefix_cols is set to the number of key columns in the index,
+ * we're as aggressive as possible about adding skip scan arrays/scan keys.
+ * This is the current default behavior, and the behavior we're targeting for
+ * the committed patch (if there are slowdowns from being maximally aggressive
+ * here then the likely solution is to make _bt_advance_array_keys adaptive,
+ * rather than trying to predict what will work during preprocessing).
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using opclass skip
+ * support routines.  This can be used to quantify the peformance benefit that
+ * comes from having dedicated skip support, with a given test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support */
+	bool		use_sksup;		/* sksup set to valid routine? */
+	Oid			eq_op;			/* InvalidOid means don't skip */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -62,17 +103,48 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
-static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan);
+static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts);
+static bool _bt_skip_support(Relation rel, int add_skip_attno,
+							 BTSkipPreproc *skipatts);
+static inline Datum _bt_apply_decrement(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array);
+static inline Datum _bt_apply_increment(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
-										   Datum arrdatum, ScanKey cur);
+										   Datum arrdatum, bool arrnull,
+										   ScanKey cur);
+static void _bt_apply_compare_array(ScanKey arraysk, ScanKey skey,
+									FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_apply_compare_skiparray(IndexScanDesc scan, ScanKey arraysk,
+										ScanKey skey, FmgrInfo *orderproc,
+										FmgrInfo *orderprocp,
+										BTArrayKeyInfo *array, bool *qual_ok);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static bool _bt_scankey_skip_increment(Relation rel, ScanDirection dir,
+									   BTArrayKeyInfo *array, ScanKey skey,
+									   FmgrInfo *orderproc);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -251,9 +323,6 @@ _bt_freestack(BTStack stack)
  * It is convenient for _bt_preprocess_keys caller to have to deal with no
  * more than one equality strategy array scan key per index attribute.  We'll
  * always be able to set things up that way when complete opfamilies are used.
- * Eliminated array scan keys can be recognized as those that have had their
- * sk_strategy field set to InvalidStrategy here by us.  Caller should avoid
- * including these in the scan's so->keyData[] output array.
  *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
@@ -261,18 +330,36 @@ _bt_freestack(BTStack stack)
  * preprocessing steps are complete.  This will convert the scan key offset
  * references into references to the scan's so->keyData[] output scan keys.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by later preprocessing on output.
+ * _bt_decide_skipatts decides which attributes receive skip arrays.
+ *
+ * Caller must pass *numberOfKeys to give us a way to change the number of
+ * input scan keys (our output is caller's input).  The returned array can be
+ * smaller than scan->keyData[] when we eliminated a redundant array scan key
+ * (redundant with some other array scan key, for the same attribute).  It can
+ * also be larger when we added a skip array/skip scan key.  Caller uses this
+ * to allocate so->keyData[] for the current btrescan.
+ *
  * Note: the reason we need to return a temp scan key array, rather than just
  * scribbling on scan->keyData, is that callers are permitted to call btrescan
  * without supplying a new set of scankey data.
  */
 static ScanKey
-_bt_preprocess_array_keys(IndexScanDesc scan)
+_bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
+	int			numArrayKeyData = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
-	int			numArrayKeys;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
+	int			numArrayKeys,
+				numSkipArrayKeys,
+				output_ikey = 0;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -280,11 +367,14 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
+	Assert(scan->numberOfKeys);
 
-	/* Quick check to see if there are any array keys */
+	/*
+	 * Quick check to see if there are any array keys, or any missing keys we
+	 * can generate a "skip scan" array key for ourselves
+	 */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
 		cur = &scan->keyData[i];
 		if (cur->sk_flags & SK_SEARCHARRAY)
@@ -300,6 +390,16 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		}
 	}
 
+	/* Consider generating skip arrays, and associated equality scan keys */
+	numSkipArrayKeys = _bt_decide_skipatts(scan, skipatts);
+	if (numSkipArrayKeys)
+	{
+		/* At least one skip array scan key must be added to arrayKeyData[] */
+		numArrayKeys += numSkipArrayKeys;
+		/* output scan key buffer allocation needs space for skip scan keys */
+		numArrayKeyData += numSkipArrayKeys;
+	}
+
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
@@ -317,19 +417,23 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
-	/* Create modifiable copy of scan->keyData in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
-	memcpy(arrayKeyData, scan->keyData, numberOfKeys * sizeof(ScanKeyData));
+	/* Create output scan keys in the workspace context */
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/*
+	 * Process each array key, and generate skip arrays as needed.  Also copy
+	 * every scan->keyData[] input scan key (whether it's an array or not)
+	 * into the arrayKeyData array we'll return to our caller (barring any
+	 * array scan keys that we could eliminate early through array merging).
+	 */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -345,14 +449,88 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		int			num_nonnulls;
 		int			j;
 
-		cur = &arrayKeyData[i];
-		if (!(cur->sk_flags & SK_SEARCHARRAY))
-			continue;
+		/* Create a skip array and scan key where indicated by skipatts */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* won't skip using this attribute */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[output_ikey];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * Temporary testing GUC can disable the use of an opclass's skip
+			 * support routine
+			 */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how the
+			 * consed-up "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[output_ikey], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			output_ikey++;		/* keep this scan key/array */
+		}
 
 		/*
-		 * First, deconstruct the array into elements.  Anything allocated
-		 * here (including a possibly detoasted array value) is in the
-		 * workspace context.
+		 * Copy input scan key into temp arrayKeyData scan key array.  (From
+		 * here on, cur points at our copy of the input scan key.)
+		 */
+		cur = &arrayKeyData[output_ikey];
+		*cur = scan->keyData[input_ikey];
+
+		if (!(cur->sk_flags & SK_SEARCHARRAY))
+		{
+			output_ikey++;		/* keep this non-array scan key */
+			continue;
+		}
+
+		/*
+		 * Deconstruct the array into elements
 		 */
 		arrayval = DatumGetArrayTypeP(cur->sk_argument);
 		/* We could cache this data, but not clear it's worth it */
@@ -406,6 +584,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
+				output_ikey++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -416,6 +595,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
+				output_ikey++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -432,7 +612,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[i], &sortprocp);
+							&so->orderProcs[output_ikey], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -476,11 +656,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					break;
 				}
 
-				/*
-				 * Indicate to _bt_preprocess_keys caller that it must ignore
-				 * this scan key
-				 */
-				cur->sk_strategy = InvalidStrategy;
+				/* Throw away this array */
 				continue;
 			}
 
@@ -511,12 +687,19 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = i;
+		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
 		numArrayKeys++;
+		output_ikey++;			/* keep this scan key/array */
 	}
 
+	/* Set final number of arrayKeyData[] keys, array keys */
+	*numberOfKeys = output_ikey;
 	so->numArrayKeys = numArrayKeys;
 
 	MemoryContextSwitchTo(oldContext);
@@ -624,7 +807,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -685,6 +869,241 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_decide_skipatts() -- set index attributes requiring skip arrays
+ *
+ * _bt_preprocess_array_keys helper function.  Determines which attributes
+ * will require skip arrays/scan keys.  Also sets up skip support function for
+ * each of these attributes.
+ *
+ * This sets up "skip scan".  Adding skip arrays (and associated scan keys)
+ * allows _bt_preprocess_keys to mark lower-order scan keys (copied from the
+ * original scan->keyData[] array in the conventional way) as required.  The
+ * overall effect is to enable skipping over irrelevant sections of the index.
+ *
+ * Return value is the total number of scan keys to add as "input" scan keys
+ * for further processing within _bt_preprocess_keys.
+ */
+static int
+_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts)
+{
+	Relation	rel = scan->indexRelation;
+	ScanKey		inputsk;
+	AttrNumber	attno_inputsk = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numSkipArrayKeys = 0,
+				prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * FIXME Also don't support parallel scans for now.  Must add logic to
+	 * places like _bt_parallel_primscan_schedule so that we account for skip
+	 * arrays when parallel workers serialize their array scan state.
+	 */
+	if (scan->parallel_scan)
+		return 0;
+
+	inputsk = &scan->keyData[0];
+	for (int i = 0;; inputsk++, i++)
+	{
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inputsk
+		 */
+		while (attno_skip < attno_inputsk)
+		{
+			if (!_bt_skip_support(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Opclass lacks a suitable skip support routine.
+				 *
+				 * Return prev_numSkipArrayKeys, so as to avoid including any
+				 * "backfilled" arrays that were supposed to form a contiguous
+				 * group with a skip array on this attribute.  There is no
+				 * benefit to adding backfill skip arrays unless we can do so
+				 * for all attributes (all attributes up to and including the
+				 * one immediately before attno_inputsk).
+				 */
+				return prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			numSkipArrayKeys++;
+			attno_skip++;
+		}
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i >= scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 * (either in part or in whole)
+		 */
+		if (attno_inputsk > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inputsk (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inputsk < inputsk->sk_attno)
+		{
+			prev_numSkipArrayKeys = numSkipArrayKeys;
+
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (!attno_has_equal)
+			{
+				/* Only saw inequalities for the prior attribute */
+				if (_bt_skip_support(rel, attno_skip,
+									 &skipatts[attno_skip - 1]))
+				{
+					/* add a range skip array for this attribute */
+					numSkipArrayKeys++;
+				}
+				else
+					break;
+			}
+			else
+			{
+				/*
+				 * Saw an equality for the prior attribute, so it doesn't need
+				 * a skip array (not even a range skip array).  We'll be able
+				 * to add later skip arrays, too (doesn't matter if the prior
+				 * attribute uses an input opclass without skip support).
+				 */
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inputsk = inputsk->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this scan key's attribute has any equality strategy scan
+		 * keys.
+		 *
+		 * Treat IS NULL scan keys as using equal strategy (they'll be marked
+		 * as using it later on, by _bt_fix_scankey_strategy).
+		 */
+		if (inputsk->sk_strategy == BTEqualStrategyNumber ||
+			(inputsk->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.
+		 *
+		 * We do still backfill skip attributes before the RowCompare, so that
+		 * it can be marked required.  This is similar to what happens when a
+		 * conventional inequality uses an opclass that lacks skip support.
+		 */
+		if (inputsk->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	return numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skip_support() -- set up skip support function in *skipatts
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false.
+ */
+static bool
+_bt_skip_support(Relation rel, int add_skip_attno, BTSkipPreproc *skipatts)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator */
+	skipatts->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										  BTEqualStrategyNumber);
+
+	/*
+	 * We don't expect input opclasses lacking even an equality operator, but
+	 * it's possible.  Deal with it gracefully.
+	 */
+	if (!OidIsValid(skipatts->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatts->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+														reverse,
+														&skipatts->sksup);
+
+	/* might not have set up skip support routine, but can skip either way */
+	return true;
+}
+
+/*
+ * _bt_apply_decrement() -- Get a decremented copy of skey's arg
+ *
+ * Note: this wrapper function calls the opclass increment function when the
+ * index stores values in descending order.  We're "logically decrementing" to
+ * the previous value in the key space regardless.
+ */
+static inline Datum
+_bt_apply_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	if (!(skey->sk_flags & SK_BT_DESC))
+		return array->sksup.decrement(rel, skey->sk_argument);
+	else
+		return array->sksup.increment(rel, skey->sk_argument);
+}
+
+/*
+ * _bt_apply_increment() -- Get an incremented copy of skey's arg
+ *
+ * Note: this wrapper function calls the opclass decrement function when the
+ * index stores values in descending order.  We're "logically incrementing" to
+ * the next value in the key space regardless.
+ */
+static inline Datum
+_bt_apply_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	if (!(skey->sk_flags & SK_BT_DESC))
+		return array->sksup.increment(rel, skey->sk_argument);
+	else
+		return array->sksup.decrement(rel, skey->sk_argument);
+}
+
 /*
  * _bt_setup_array_cmp() -- Set up array comparison functions
  *
@@ -979,15 +1398,10 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
@@ -1000,8 +1414,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.  We have to be sure
+	 * that _bt_compare_array_skey/_bt_binsrch_array_skey use the right proc.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1032,11 +1446,45 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	/*
+	 * We have all we need to determine redundancy/contradictoriness.
+	 *
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+		_bt_apply_compare_array(arraysk, skey,
+								orderprocp, array, qual_ok);
+	else
+		_bt_apply_compare_skiparray(scan, arraysk, skey, orderproc,
+									orderprocp, array, qual_ok);
+
+	return true;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when it
+ * is redundant with (or contradicted by) a non-array scalar scan key.
+ *
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ */
+static void
+_bt_apply_compare_array(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1088,8 +1536,175 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
 
-	return true;
+/*
+ * Finish off preprocessing of skip array scan key when it is redundant with
+ * (or contradicted by) a non-array scalar scan key.
+ *
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Arrays used to skip (skip scan/missing key attribute predicates) work by
+ * procedurally generating their elements on the fly.  We must still
+ * "eliminate contradictory elements", but it works a little differently: we
+ * narrow the range of the skip array, such that the array will never
+ * generated contradicted-by-skey elements.
+ *
+ * FIXME Our behavior in scenarios with cross-type operators (range skip scan
+ * cases) is buggy.  We're naively copying datums of a different type from
+ * scalar inequality scan keys into the array's low_value and high_value
+ * fields.  In practice this tends to not visibly break (in practice types
+ * that appear within the same operator family tend to have compatible datum
+ * representations, at least on systems with little-endian byte order).  Put
+ * off dealing with the problem until a later revision of the patch.
+ *
+ * It seems likely that the best way to fix this problem will involve keeping
+ * around the original operator in the BTArrayKeyInfo array struct whenever
+ * we're passed a "redundant" cross-type inequality operator (an approach
+ * involving casts/coercions might be tempting, but seems much too fragile).
+ * We only need to use not-column-input-opclass-type operators for the first
+ * and/or last array elements from the skip array under this scheme; we'll
+ * still mostly be dealing with opcintype-typed datums, copied from the index
+ * (as well as incrementing/decrementing copies of those index tuple datums).
+ * Importantly, this scheme should work just as well with an opfamily that
+ * doesn't even have an orderprocp cross-type ORDER operator to pass us here
+ * (we might even have to keep more than one same-strategy inequality, since
+ * in general _bt_preprocess_keys might not be able to prove which inequality
+ * is redundant).
+ */
+static void
+_bt_apply_compare_skiparray(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							FmgrInfo *orderproc, FmgrInfo *orderprocp,
+							BTArrayKeyInfo *array, bool *qual_ok)
+{
+	Relation	rel = scan->indexRelation;
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	Form_pg_attribute attr = TupleDescAttr(RelationGetDescr(rel),
+										   skey->sk_attno - 1);
+	MemoryContext oldContext;
+	int			cmpresult;
+
+	/*
+	 * We don't expect to have to deal with NULLs in non-array/non-skip scan
+	 * key.  We expect _bt_preprocess_array_keys to avoid generating a skip
+	 * array for an index attribute with an IS NULL input scan key. It will
+	 * still do so in the presence of IS NOT NULL input scan keys, but
+	 * _bt_compare_scankey_args is expected to handle those for us.
+	 */
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(arraysk->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & SK_ISNULL));
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Scalar scan key must be a B-Tree operator, which must always be strict.
+	 * Array shouldn't generate a NULL "array element"/an IS NULL qual.  This
+	 * isn't just an optimization; it's strictly necessary for correctness.
+	 */
+	array->null_elem = false;
+
+	if (!array->use_sksup)
+	{
+		switch (skey->sk_strategy)
+		{
+			case BTLessStrategyNumber:
+			case BTLessEqualStrategyNumber:
+				array->high_compare = MemoryContextAlloc(so->arrayContext,
+														 sizeof(ScanKeyData));
+				memcpy(array->high_compare, skey, sizeof(ScanKeyData));
+				break;
+			case BTGreaterEqualStrategyNumber:
+			case BTGreaterStrategyNumber:
+				array->low_compare = MemoryContextAlloc(so->arrayContext,
+														sizeof(ScanKeyData));
+				memcpy(array->low_compare, skey, sizeof(ScanKeyData));
+				break;
+			default:
+				elog(ERROR, "unrecognized StrategyNumber: %d",
+					 (int) skey->sk_strategy);
+				break;
+		}
+
+		array->null_elem = false;
+		*qual_ok = true;
+
+		return;
+	}
+
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+
+			/*
+			 * detect if scan key argument will be < low_value once
+			 * decremented
+			 */
+			cmpresult = _bt_compare_array_skey(orderprocp,
+											   skey->sk_argument, false,
+											   array->sksup.low_elem, false,
+											   arraysk);
+			if (cmpresult <= 0)
+			{
+				/* decrementing would make qual unsatisfiable, so don't try */
+				*qual_ok = false;
+				return;
+			}
+
+			/* decremented scan key value becomes skip array's new high_value */
+			oldContext = MemoryContextSwitchTo(so->arrayContext);
+			array->sksup.high_elem = _bt_apply_decrement(rel, skey, array);
+			MemoryContextSwitchTo(oldContext);
+			break;
+		case BTLessEqualStrategyNumber:
+			oldContext = MemoryContextSwitchTo(so->arrayContext);
+			array->sksup.high_elem = datumCopy(skey->sk_argument,
+											   attr->attbyval, attr->attlen);
+			MemoryContextSwitchTo(oldContext);
+			break;
+		case BTGreaterEqualStrategyNumber:
+			oldContext = MemoryContextSwitchTo(so->arrayContext);
+			array->sksup.low_elem = datumCopy(skey->sk_argument,
+											  attr->attbyval, attr->attlen);
+			MemoryContextSwitchTo(oldContext);
+			break;
+		case BTGreaterStrategyNumber:
+
+			/*
+			 * detect if scan key argument will be > high_value once
+			 * incremented
+			 */
+			cmpresult = _bt_compare_array_skey(orderprocp,
+											   skey->sk_argument, false,
+											   array->sksup.high_elem, false,
+											   arraysk);
+			if (cmpresult >= 0)
+			{
+				/* incrementing would make qual unsatisfiable, so don't try */
+				*qual_ok = false;
+				return;
+			}
+
+			/* incremented scan key value becomes skip array's new low_value */
+			oldContext = MemoryContextSwitchTo(so->arrayContext);
+			array->sksup.low_elem = _bt_apply_increment(rel, skey, array);
+			MemoryContextSwitchTo(oldContext);
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	/*
+	 * Is the qual contradictory, or is it merely "redundant" with consed-up
+	 * skip array?
+	 */
+	cmpresult = _bt_compare_array_skey(orderproc,	/* don't use orderprocp */
+									   array->sksup.low_elem, false,
+									   array->sksup.high_elem, false,
+									   arraysk);
+	*qual_ok = (cmpresult <= 0);
 }
 
 /*
@@ -1130,7 +1745,8 @@ _bt_compare_array_elements(const void *a, const void *b, void *arg)
 static inline int32
 _bt_compare_array_skey(FmgrInfo *orderproc,
 					   Datum tupdatum, bool tupnull,
-					   Datum arrdatum, ScanKey cur)
+					   Datum arrdatum, bool arrnull,
+					   ScanKey cur)
 {
 	int32		result = 0;
 
@@ -1138,14 +1754,14 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 
 	if (tupnull)				/* NULL tupdatum */
 	{
-		if (cur->sk_flags & SK_ISNULL)
+		if (arrnull)
 			result = 0;			/* NULL "=" NULL */
 		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = -1;		/* NULL "<" NOT_NULL */
 		else
 			result = 1;			/* NULL ">" NOT_NULL */
 	}
-	else if (cur->sk_flags & SK_ISNULL) /* NOT_NULL tupdatum, NULL arrdatum */
+	else if (arrnull)			/* NOT_NULL tupdatum, NULL arrdatum */
 	{
 		if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = 1;			/* NOT_NULL ">" NULL */
@@ -1211,6 +1827,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1246,7 +1864,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[low_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result <= 0)
 				{
@@ -1274,7 +1892,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[high_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result >= 0)
 				{
@@ -1301,7 +1919,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 		arrdatum = array->elem_values[mid_elem];
 
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										arrdatum, cur);
+										arrdatum, false, cur);
 
 		if (result == 0)
 		{
@@ -1326,13 +1944,196 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	 */
 	if (low_elem != mid_elem)
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										array->elem_values[low_elem], cur);
+										array->elem_values[low_elem], false,
+										cur);
 
 	*set_elem_result = result;
 
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Skip scan arrays procedurally generate their elements on-demand.  They
+ * largely function in the same way as standard arrays.  They can be rolled
+ * over by standard arrays (standard array can also roll over skip arrays).
+ *
+ * This routine doesn't return an index into the array, because the array
+ * doesn't actually have any elements (it has low_value and high_value, which
+ * indicate the range of values that the array can generate).  Note that this
+ * may include a NULL value/an IS NULL qual (unlike with true arrays).
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this skip
+ * array's scan key.  We can apply this information to find the next matching
+ * array element in the current scan direction using fewer comparisons.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Datum		arrdatum;
+	bool		arrnull;
+
+	Assert(!ScanDirectionIsNoMovement(dir));
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+
+	/* Precheck for NULL tupdatum, array without a NULL element */
+	if (tupnull && !array->null_elem)
+	{
+		if (!(cur->sk_flags & SK_BT_NULLS_FIRST))
+			*set_elem_result = 1;
+		else
+			*set_elem_result = -1;
+
+		return;
+	}
+
+	/*
+	 * Compare tupdatum against "first array element" in the current scan
+	 * direction first (and allow NULL to be treated as a possible element).
+	 *
+	 * Optimization: don't have to bother with this when passed a skip array
+	 * that is known to have triggered array advancement.
+	 */
+	if (!cur_elem_trig)
+	{
+		if (array->use_sksup)
+		{
+			if (ScanDirectionIsForward(dir))
+			{
+				arrdatum = array->sksup.low_elem;
+				arrnull = array->null_elem &&
+					(cur->sk_flags & SK_BT_NULLS_FIRST);
+			}
+			else
+			{
+				arrdatum = array->sksup.high_elem;
+				arrnull = array->null_elem &&
+					!(cur->sk_flags & SK_BT_NULLS_FIRST);
+			}
+
+			*set_elem_result = _bt_compare_array_skey(orderproc,
+													  tupdatum, tupnull,
+													  arrdatum, arrnull, cur);
+
+			/*
+			 * Optimization: return early when >= lower bound happens to be an
+			 * exact match (or when <= upper bound is an exact match during a
+			 * backwards scan)
+			 */
+			if (*set_elem_result == 0)
+				return;
+		}
+		else
+		{
+			*set_elem_result = 0;	/* for now */
+
+			if (ScanDirectionIsForward(dir) && array->low_compare)
+			{
+				ScanKey		low_compare = array->low_compare;
+
+				if (!DatumGetBool(FunctionCall2Coll(&low_compare->sk_func,
+													low_compare->sk_collation,
+													tupdatum,
+													low_compare->sk_argument)))
+					*set_elem_result = -1;
+			}
+			else if (ScanDirectionIsBackward(dir) && array->high_compare)
+			{
+				ScanKey		high_compare = array->high_compare;
+
+				if (!DatumGetBool(FunctionCall2Coll(&high_compare->sk_func,
+													high_compare->sk_collation,
+													tupdatum,
+													high_compare->sk_argument)))
+					*set_elem_result = 1;
+			}
+		}
+
+		/* tupdatum before the start of first element in scan direction? */
+		if ((ScanDirectionIsForward(dir) && *set_elem_result < 0) ||
+			(ScanDirectionIsBackward(dir) && *set_elem_result > 0))
+			return;
+	}
+
+	/*
+	 * Now compare tupdatum to the last array element in the current scan
+	 * direction (and allow NULL to be treated as a possible element)
+	 */
+	if (array->use_sksup)
+	{
+		/*
+		 * We have skip support, so there is literally a final element
+		 */
+		if (ScanDirectionIsForward(dir))
+		{
+			arrdatum = array->sksup.high_elem;
+			arrnull = array->null_elem && !(cur->sk_flags & SK_BT_NULLS_FIRST);
+		}
+		else
+		{
+			arrdatum = array->sksup.low_elem;
+			arrnull = array->null_elem && (cur->sk_flags & SK_BT_NULLS_FIRST);
+		}
+		*set_elem_result = _bt_compare_array_skey(orderproc,
+												  tupdatum, tupnull,
+												  arrdatum, arrnull, cur);
+	}
+	else
+	{
+		*set_elem_result = 0;	/* for now */
+
+		/*
+		 * No skip support.  Need to use any inequalities required in the
+		 * current scan direction as demarcating where the final element is.
+		 */
+		if (ScanDirectionIsForward(dir) && array->high_compare)
+		{
+			ScanKey		high_compare = array->high_compare;
+
+			if (!DatumGetBool(FunctionCall2Coll(&high_compare->sk_func,
+												high_compare->sk_collation,
+												tupdatum,
+												high_compare->sk_argument)))
+				*set_elem_result = 1;
+		}
+		else if (ScanDirectionIsBackward(dir) && array->low_compare)
+		{
+			ScanKey		low_compare = array->low_compare;
+
+			if (!DatumGetBool(FunctionCall2Coll(&low_compare->sk_func,
+												low_compare->sk_collation,
+												tupdatum,
+												low_compare->sk_argument)))
+				*set_elem_result = -1;
+		}
+	}
+
+	/* tupdatum after the end of final element in scan direction? */
+	if ((ScanDirectionIsForward(dir) && *set_elem_result > 0) ||
+		(ScanDirectionIsBackward(dir) && *set_elem_result < 0))
+		return;
+
+	/*
+	 * tupdatum is within the range of the skip array.  This is equivalent to
+	 * _bt_binsrch_array_skey finding an exactly matching array element.
+	 */
+	*set_elem_result = 0;
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1342,29 +2143,488 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
 		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
 		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, curArrayKey,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = false;
 }
 
+/*
+ * _bt_scankey_decrement() -- decrement scan key's sk_argument
+ *
+ * Unsets scan key "IS NULL" flags if required.  Cannot handle "decrementing"
+ * sk_argument from a non-NULL value to the value NULL.
+ */
+static void
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (skey->sk_flags & SK_ISNULL)
+		_bt_scankey_unset_isnull(rel, skey, array);
+	else
+	{
+		Datum		dec_sk_argument;
+		Form_pg_attribute attr;
+
+		/* Get a decremented copy of existing sk_argument */
+		dec_sk_argument = _bt_apply_decrement(rel, skey, array);
+
+		/* Free memory previously allocated for sk_argument if needed */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+
+		/* Set decremented copy of original sk_argument in scan key */
+		skey->sk_argument = dec_sk_argument;
+	}
+}
+
+/*
+ * _bt_scankey_increment() -- increment scan key's sk_argument
+ *
+ * Unsets scan key "IS NULL" flags if required.  Cannot handle "incrementing"
+ * sk_argument from a non-NULL value to the value NULL.
+ */
+static void
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->use_sksup);
+
+	if (skey->sk_flags & SK_ISNULL)
+		_bt_scankey_unset_isnull(rel, skey, array);
+	else
+	{
+		Datum		inc_sk_argument;
+		Form_pg_attribute attr;
+
+		/* Get an incremented copy of existing sk_argument */
+		inc_sk_argument = _bt_apply_increment(rel, skey, array);
+
+		/* Free memory previously allocated for sk_argument if needed */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+
+		/* Set incremented copy of original sk_argument in scan key */
+		skey->sk_argument = inc_sk_argument;
+	}
+}
+
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Clear possibly-irrelevant flags (before possible setting some again) */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEG_INF | SK_BT_POS_INF |
+						SK_BT_NEXTKEY | SK_BT_PREVKEY);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Set element to NULL (lowest/highest element) */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Lowest array element isn't NULL */
+		ScanKey		low_compare = array->low_compare;
+
+		if (array->use_sksup)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else if (!low_compare)
+			skey->sk_flags |= SK_BT_NEG_INF;
+		else if (low_compare->sk_subtype != InvalidOid &&
+				 low_compare->sk_subtype !=
+				 rel->rd_opcintype[skey->sk_attno - 1])
+		{
+			/* XXX papers-over lack of cross-type support in _bt_first */
+			skey->sk_flags |= SK_BT_NEG_INF;
+		}
+		else
+		{
+			skey->sk_argument = datumCopy(low_compare->sk_argument,
+										  attr->attbyval, attr->attlen);
+
+			if (low_compare->sk_strategy == BTGreaterStrategyNumber)
+				skey->sk_flags |= SK_BT_NEXTKEY;
+		}
+	}
+	else
+	{
+		/* Highest array element isn't NULL */
+		ScanKey		high_compare = array->high_compare;
+
+		if (array->use_sksup)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else if (!high_compare)
+			skey->sk_flags |= SK_BT_POS_INF;
+		else if (high_compare->sk_subtype != InvalidOid &&
+				 high_compare->sk_subtype !=
+				 rel->rd_opcintype[skey->sk_attno - 1])
+		{
+			/* XXX papers-over lack of cross-type support in _bt_first */
+			skey->sk_flags |= SK_BT_POS_INF;
+		}
+		else
+		{
+			skey->sk_argument = datumCopy(high_compare->sk_argument,
+										  attr->attbyval, attr->attlen);
+			if (high_compare->sk_strategy == BTLessStrategyNumber)
+				skey->sk_flags |= SK_BT_PREVKEY;
+		}
+	}
+}
+
+/*
+ * _bt_scankey_skip_increment() -- increment a skip scan key, and its array
+ *
+ * Returns true when the skip array was successfully incremented to the next
+ * value in the current scan direction, dir.  Otherwise handles roll over by
+ * setting array to its final element for the current scan direction.
+ */
+static bool
+_bt_scankey_skip_increment(Relation rel, ScanDirection dir,
+						   BTArrayKeyInfo *array, ScanKey skey,
+						   FmgrInfo *orderproc)
+{
+	Datum		sk_argument = skey->sk_argument;
+	bool		sk_isnull = (skey->sk_flags & SK_ISNULL) != 0;
+	int			compare;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Precheck for the sentinel values -inf and +inf.  These values are only
+	 * used for index columns whose input operator class doesn't provide its
+	 * own skip support routine.
+	 */
+	Assert(!(skey->sk_flags & SK_BT_POS_INF) || ScanDirectionIsForward(dir));
+	Assert(!(skey->sk_flags & SK_BT_NEG_INF) || ScanDirectionIsBackward(dir));
+	if (skey->sk_flags & (SK_BT_POS_INF | SK_BT_NEG_INF))
+	{
+		Assert(!array->use_sksup);
+		goto rollover;
+	}
+
+	skey->sk_flags &= ~(SK_BT_NEXTKEY | SK_BT_PREVKEY);
+
+	if (ScanDirectionIsForward(dir))
+	{
+		if (array->high_compare)
+		{
+			ScanKey		high_compare = array->high_compare;
+
+			Assert(!array->use_sksup);
+			Assert(!array->null_elem && !sk_isnull);
+
+			if (high_compare->sk_strategy == BTLessEqualStrategyNumber)
+			{
+				/* XXX Need to consider cross-type operator families here */
+				compare = _bt_compare_array_skey(orderproc,
+												 high_compare->sk_argument, false,
+												 sk_argument, sk_isnull, skey);
+				if (compare <= 0)
+					goto rollover;
+			}
+			else if (!DatumGetBool(FunctionCall2Coll(&high_compare->sk_func,
+													 high_compare->sk_collation,
+													 sk_argument,
+													 high_compare->sk_argument)))
+				goto rollover;
+		}
+
+		if (!array->use_sksup)
+		{
+			/*
+			 * Optimization: when the current array element is NULL, and the
+			 * last item stored in the index is also NULL, treat NULL as the
+			 * final array element (final when scanning forwards).
+			 *
+			 * This saves a useless primitive index scan that would otherwise
+			 * try to locate a value after NULL.
+			 */
+			if (sk_isnull && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+				goto rollover;
+
+			/* "Increment" sk_argument to sentinel value */
+			skey->sk_flags |= SK_BT_NEXTKEY;
+			return true;
+		}
+
+		/* high_elem is final non-NULL element in current scan direction */
+		compare = _bt_compare_array_skey(orderproc,
+										 array->sksup.high_elem, false,
+										 sk_argument, sk_isnull, skey);
+		if (compare > 0)
+		{
+			/* Increment sk_argument to next non-NULL array element */
+			_bt_scankey_increment(rel, skey, array);
+
+			return true;
+		}
+		else if (compare == 0 && array->null_elem &&
+				 !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/*
+			 * Existing sk_argument is already equal to non-NULL high_elem,
+			 * but skip array's true highest element is actually NULL.
+			 *
+			 * "Increment" sk_argument to NULL.
+			 */
+			_bt_scankey_set_isnull(rel, skey, array);
+
+			return true;
+		}
+
+		/* Exhausted all array elements in current scan direction */
+	}
+	else
+	{
+		if (array->low_compare)
+		{
+			ScanKey		low_compare = array->low_compare;
+
+			Assert(!array->use_sksup);
+			Assert(!array->null_elem && !sk_isnull);
+
+			if (low_compare->sk_strategy == BTGreaterEqualStrategyNumber)
+			{
+				/* XXX Need to consider cross-type operator families here */
+				compare = _bt_compare_array_skey(orderproc,
+												 low_compare->sk_argument, false,
+												 sk_argument, sk_isnull, skey);
+				if (compare >= 0)
+					goto rollover;
+			}
+			else if (!DatumGetBool(FunctionCall2Coll(&low_compare->sk_func,
+													 low_compare->sk_collation,
+													 sk_argument,
+													 low_compare->sk_argument)))
+				goto rollover;
+		}
+
+		if (!array->use_sksup)
+		{
+			/*
+			 * Optimization: when the current array element is NULL, and the
+			 * first item stored in the index is also NULL, treat NULL as the
+			 * final array element (final when scanning backwards).
+			 *
+			 * This saves a useless primitive index scan that would otherwise
+			 * try to locate a value before NULL.
+			 */
+			if (sk_isnull && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+				goto rollover;
+
+			/* "Decrement" sk_argument to sentinel value */
+			skey->sk_flags |= SK_BT_PREVKEY;
+			return true;
+		}
+
+		/* low_elem is final non-NULL element in current scan direction */
+		compare = _bt_compare_array_skey(orderproc,
+										 array->sksup.low_elem, false,
+										 sk_argument, sk_isnull, skey);
+		if (compare < 0)
+		{
+			/* Decrement sk_argument to previous non-NULL array element */
+			_bt_scankey_decrement(rel, skey, array);
+
+			return true;
+		}
+		else if (compare == 0 && array->null_elem &&
+				 (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/*
+			 * Existing sk_argument is already equal to non-NULL low_elem, but
+			 * skip array's true lowest element is actually NULL.
+			 *
+			 * "Decrement" sk_argument to NULL.
+			 */
+			_bt_scankey_set_isnull(rel, skey, array);
+
+			return true;
+		}
+
+		/* Exhausted all array elements in current scan direction */
+	}
+
+	/*
+	 * Skip array rolls over.  Start over at the array's lowest sorting value
+	 * (or its highest value, for backward scans).
+	 */
+rollover:
+
+	_bt_scankey_set_low_or_high(rel, skey, array, ScanDirectionIsForward(dir));
+
+	/* Caller must consider earlier/more significant arrays in turn */
+	return false;
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEG_INF | SK_BT_POS_INF |
+						SK_BT_NEXTKEY | SK_BT_PREVKEY);
+
+	/*
+	 * Treat tupdatum/tupnull as a matching array element.
+	 *
+	 * We just copy tupdatum into the array's scan key (there is no
+	 * conventional array element for us to set, of course).
+	 *
+	 * Unlike standard arrays, skip arrays sometimes need to locate NULLs.
+	 * Treat them as just another value from the domain of indexed values.
+	 */
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(array->use_sksup);
+	Assert(array->null_elem);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEG_INF | SK_BT_POS_INF |
+						SK_BT_NEXTKEY | SK_BT_PREVKEY);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- increment/decrement scan key to NULL
+ *
+ * Sets scan key to "IS NULL", and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEG_INF | SK_BT_POS_INF |
+							   SK_BT_NEXTKEY | SK_BT_PREVKEY)));
+	Assert(array->null_elem);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1380,6 +2640,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1391,10 +2652,24 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	{
 		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
 		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		FmgrInfo   *orderproc = &so->orderProcs[curArrayKey->scan_key];
 		int			cur_elem = curArrayKey->cur_elem;
 		int			num_elems = curArrayKey->num_elems;
 		bool		rolled = false;
 
+		/* Handle incrementing a skip array */
+		if (num_elems == -1)
+		{
+			/* Attempt to incrementally advance this skip scan array */
+			if (_bt_scankey_skip_increment(rel, dir, curArrayKey, skey,
+										   orderproc))
+				return true;
+
+			/* Array rolled over.  Need to advance next array key, if any. */
+			continue;
+		}
+
+		/* Handle incrementing a true array */
 		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
 		{
 			cur_elem = 0;
@@ -1411,7 +2686,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 		if (!rolled)
 			return true;
 
-		/* Need to advance next array key, if any */
+		/* Array rolled over.  Need to advance next array key, if any. */
 	}
 
 	/*
@@ -1466,6 +2741,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1473,7 +2749,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1485,16 +2760,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No skipping of non-required arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1558,6 +2827,8 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 	for (int ikey = sktrig; ikey < so->numberOfKeys; ikey++)
 	{
 		ScanKey		cur = so->keyData + ikey;
+		Datum		sk_argument = cur->sk_argument;
+		bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
 		Datum		tupdatum;
 		bool		tupnull;
 		int32		result;
@@ -1617,11 +2888,14 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		if (cur->sk_flags & (SK_BT_NEG_INF | SK_BT_POS_INF))
+			return false;
+
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
 		result = _bt_compare_array_skey(&so->orderProcs[ikey],
 										tupdatum, tupnull,
-										cur->sk_argument, cur);
+										sk_argument, sk_isnull, cur);
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1631,6 +2905,9 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 			(ScanDirectionIsBackward(dir) && result > 0))
 			return true;
 
+		if ((cur->sk_flags & (SK_BT_NEXTKEY | SK_BT_PREVKEY)) && result == 0)
+			return true;
+
 		/*
 		 * Does this comparison indicate that caller should now advance the
 		 * scan's arrays?  (Must be if we get here during a readpagetup call.)
@@ -1954,18 +3231,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1990,18 +3258,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2019,15 +3278,27 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * Skip array.  "Binary search" by checking if tupdatum/tupnull
+			 * are within the low_value/high_value range of the skip array.
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
+			Datum		sk_argument = cur->sk_argument;
+			bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
+
 			Assert(sktrig_required && required);
 
 			/*
@@ -2041,7 +3312,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 */
 			result = _bt_compare_array_skey(&so->orderProcs[ikey],
 											tupdatum, tupnull,
-											cur->sk_argument, cur);
+											sk_argument, sk_isnull, cur);
 		}
 
 		/*
@@ -2100,11 +3371,62 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+
+		if (!array)
+			continue;			/* no element to set in non-array */
+
+		/* Conventional arrays have a valid set_elem for us to advance to */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * Conceptually, skip arrays also have array elements.  The actual
+		 * elements/values are generated procedurally and on demand.
+		 */
+		Assert(cur->sk_flags & SK_BT_SKIP);
+		Assert(array->num_elems == -1);
+		Assert(required);
+
+		if (result == 0)
+		{
+			/*
+			 * Anything within the range of possible element values is treated
+			 * as "a match for one of the array's elements".  Store the next
+			 * scan key argument value by taking a copy of the tupdatum value
+			 * from caller's tuple (or set scan key IS NULL when tupnull, iff
+			 * the array's range of possible elements covers NULL).
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
+		}
+		else if (beyond_end_advance)
+		{
+			/*
+			 * We need to set the array element to the final "element" in the
+			 * current scan direction for "beyond end of array element" array
+			 * advancement.  See above for an explanation.
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else
+		{
+			/*
+			 * The closest matching element is the lowest element; even that
+			 * still puts us ahead of caller's tuple in the key space.  This
+			 * process has to carry to any lower-order arrays.  See above for
+			 * an explanation.
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
 		}
 	}
 
@@ -2460,10 +3782,12 @@ end_toplevel_scan:
 /*
  *	_bt_preprocess_keys() -- Preprocess scan keys
  *
+ * The first call here (per btrescan) allocates so->keyData[].
  * The given search-type keys (taken from scan->keyData[])
  * are copied to so->keyData[] with possible transformation.
  * scan->numberOfKeys is the number of input keys, so->numberOfKeys gets
- * the number of output keys (possibly less, never greater).
+ * the number of output keys.  Calling here a second time (during the same
+ * btrescan) is a no-op.
  *
  * The output keys are marked with additional sk_flags bits beyond the
  * system-standard bits supplied by the caller.  The DESC and NULLS_FIRST
@@ -2483,6 +3807,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also cons up skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2550,9 +3876,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	int16	   *indoption = scan->indexRelation->rd_indoption;
 	int			new_numberOfKeys;
 	int			numberOfEqualCols;
-	ScanKey		inkeys;
-	ScanKey		outkeys;
-	ScanKey		cur;
+	ScanKey		inputsk;
 	BTScanKeyPreproc xform[BTMaxStrategyNumber];
 	bool		test_result;
 	int			i,
@@ -2584,7 +3908,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		return;					/* done if qual-less scan */
 
 	/* If any keys are SK_SEARCHARRAY type, set up array-key info */
-	arrayKeyData = _bt_preprocess_array_keys(scan);
+	arrayKeyData = _bt_preprocess_array_keys(scan, &numberOfKeys);
 	if (!so->qual_ok)
 	{
 		/* unmatchable array, so give up */
@@ -2598,32 +3922,36 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	 */
 	if (arrayKeyData)
 	{
-		inkeys = arrayKeyData;
+		inputsk = arrayKeyData;
 
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
 	}
 	else
-		inkeys = scan->keyData;
+		inputsk = scan->keyData;
+
+	/*
+	 * Now that we have an estimate of the number of output scan keys
+	 * (including any skip array scan keys), allocate space for them
+	 */
+	so->keyData = palloc(sizeof(ScanKeyData) * numberOfKeys);
 
-	outkeys = so->keyData;
-	cur = &inkeys[0];
 	/* we check that input keys are correctly ordered */
-	if (cur->sk_attno < 1)
+	if (inputsk->sk_attno < 1)
 		elog(ERROR, "btree index keys must be ordered by attribute");
 
 	/* We can short-circuit most of the work if there's just one key */
 	if (numberOfKeys == 1)
 	{
 		/* Apply indoption to scankey (might change sk_strategy!) */
-		if (!_bt_fix_scankey_strategy(cur, indoption))
+		if (!_bt_fix_scankey_strategy(inputsk, indoption))
 			so->qual_ok = false;
-		memcpy(outkeys, cur, sizeof(ScanKeyData));
+		memcpy(so->keyData, inputsk, sizeof(ScanKeyData));
 		so->numberOfKeys = 1;
 		/* We can mark the qual as required if it's for first index col */
-		if (cur->sk_attno == 1)
-			_bt_mark_scankey_required(outkeys);
+		if (inputsk->sk_attno == 1)
+			_bt_mark_scankey_required(so->keyData);
 		if (arrayKeyData)
 		{
 			/*
@@ -2631,8 +3959,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * (we'll miss out on the single value array transformation, but
 			 * that's not nearly as important when there's only one scan key)
 			 */
-			Assert(cur->sk_flags & SK_SEARCHARRAY);
-			Assert(cur->sk_strategy != BTEqualStrategyNumber ||
+			Assert(so->keyData[0].sk_flags & SK_SEARCHARRAY);
+			Assert(so->keyData[0].sk_strategy != BTEqualStrategyNumber ||
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
@@ -2660,12 +3988,12 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	 * handle after-last-key processing.  Actual exit from the loop is at the
 	 * "break" statement below.
 	 */
-	for (i = 0;; cur++, i++)
+	for (i = 0;; inputsk++, i++)
 	{
 		if (i < numberOfKeys)
 		{
 			/* Apply indoption to scankey (might change sk_strategy!) */
-			if (!_bt_fix_scankey_strategy(cur, indoption))
+			if (!_bt_fix_scankey_strategy(inputsk, indoption))
 			{
 				/* NULL can't be matched, so give up */
 				so->qual_ok = false;
@@ -2677,12 +4005,12 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		 * If we are at the end of the keys for a particular attr, finish up
 		 * processing and emit the cleaned-up keys.
 		 */
-		if (i == numberOfKeys || cur->sk_attno != attno)
+		if (i == numberOfKeys || inputsk->sk_attno != attno)
 		{
 			int			priorNumberOfEqualCols = numberOfEqualCols;
 
 			/* check input keys are correctly ordered */
-			if (i < numberOfKeys && cur->sk_attno < attno)
+			if (i < numberOfKeys && inputsk->sk_attno < attno)
 				elog(ERROR, "btree index keys must be ordered by attribute");
 
 			/*
@@ -2741,7 +4069,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
+						Assert(!array || array->num_elems > 0 ||
+							   array->num_elems == -1);
 						xform[j].skey = NULL;
 						xform[j].ikey = -1;
 					}
@@ -2786,7 +4115,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			}
 
 			/*
-			 * Emit the cleaned-up keys into the outkeys[] array, and then
+			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
 			 */
@@ -2794,7 +4123,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			{
 				if (xform[j].skey)
 				{
-					ScanKey		outkey = &outkeys[new_numberOfKeys++];
+					ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
 					memcpy(outkey, xform[j].skey, sizeof(ScanKeyData));
 					if (arrayKeyData)
@@ -2811,19 +4140,19 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				break;
 
 			/* Re-initialize for new attno */
-			attno = cur->sk_attno;
+			attno = inputsk->sk_attno;
 			memset(xform, 0, sizeof(xform));
 		}
 
 		/* check strategy this key's operator corresponds to */
-		j = cur->sk_strategy - 1;
+		j = inputsk->sk_strategy - 1;
 
 		/* if row comparison, push it directly to the output array */
-		if (cur->sk_flags & SK_ROW_HEADER)
+		if (inputsk->sk_flags & SK_ROW_HEADER)
 		{
-			ScanKey		outkey = &outkeys[new_numberOfKeys++];
+			ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
-			memcpy(outkey, cur, sizeof(ScanKeyData));
+			memcpy(outkey, inputsk, sizeof(ScanKeyData));
 			if (arrayKeyData)
 				keyDataMap[new_numberOfKeys - 1] = i;
 			if (numberOfEqualCols == attno - 1)
@@ -2837,19 +4166,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			continue;
 		}
 
-		/*
-		 * Does this input scan key require further processing as an array?
-		 */
-		if (cur->sk_strategy == InvalidStrategy)
-		{
-			/* _bt_preprocess_array_keys marked this array key redundant */
-			Assert(arrayKeyData);
-			Assert(cur->sk_flags & SK_SEARCHARRAY);
-			continue;
-		}
-
-		if (cur->sk_strategy == BTEqualStrategyNumber &&
-			(cur->sk_flags & SK_SEARCHARRAY))
+		if (inputsk->sk_strategy == BTEqualStrategyNumber &&
+			(inputsk->sk_flags & SK_SEARCHARRAY))
 		{
 			/* _bt_preprocess_array_keys kept this array key */
 			Assert(arrayKeyData);
@@ -2863,7 +4181,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		if (xform[j].skey == NULL)
 		{
 			/* nope, so this scan key wins by default (at least for now) */
-			xform[j].skey = cur;
+			xform[j].skey = inputsk;
 			xform[j].ikey = i;
 			xform[j].arrayidx = arrayidx;
 		}
@@ -2881,7 +4199,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/*
 				 * Have to set up array keys
 				 */
-				if ((cur->sk_flags & SK_SEARCHARRAY))
+				if ((inputsk->sk_flags & SK_SEARCHARRAY))
 				{
 					array = &so->arrayKeys[arrayidx - 1];
 					orderproc = so->orderProcs + i;
@@ -2909,13 +4227,15 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				 */
 			}
 
-			if (_bt_compare_scankey_args(scan, cur, cur, xform[j].skey,
-										 array, orderproc, &test_result))
+			if (_bt_compare_scankey_args(scan, inputsk, inputsk,
+										 xform[j].skey, array, orderproc,
+										 &test_result))
 			{
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
+					Assert(!array || array->num_elems > 0 ||
+						   array->num_elems == -1);
 
 					/*
 					 * New key is more restrictive, and so replaces old key...
@@ -2923,7 +4243,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 					if (j != (BTEqualStrategyNumber - 1) ||
 						!(xform[j].skey->sk_flags & SK_SEARCHARRAY))
 					{
-						xform[j].skey = cur;
+						xform[j].skey = inputsk;
 						xform[j].ikey = i;
 						xform[j].arrayidx = arrayidx;
 					}
@@ -2936,7 +4256,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 						 * scan key.  _bt_compare_scankey_args expects us to
 						 * always keep arrays (and discard non-arrays).
 						 */
-						Assert(!(cur->sk_flags & SK_SEARCHARRAY));
+						Assert(!(inputsk->sk_flags & SK_SEARCHARRAY));
 					}
 				}
 				else if (j == (BTEqualStrategyNumber - 1))
@@ -2959,14 +4279,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				 * even with incomplete opfamilies.  _bt_advance_array_keys
 				 * depends on this.
 				 */
-				ScanKey		outkey = &outkeys[new_numberOfKeys++];
+				ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
 				memcpy(outkey, xform[j].skey, sizeof(ScanKeyData));
 				if (arrayKeyData)
 					keyDataMap[new_numberOfKeys - 1] = xform[j].ikey;
 				if (numberOfEqualCols == attno - 1)
 					_bt_mark_scankey_required(outkey);
-				xform[j].skey = cur;
+				xform[j].skey = inputsk;
 				xform[j].ikey = i;
 				xform[j].arrayidx = arrayidx;
 			}
@@ -3057,10 +4377,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3135,6 +4456,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Don't allow skip array to generate IS NULL scan key/element */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3208,6 +4545,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3380,13 +4718,6 @@ _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption)
 		return true;
 	}
 
-	if (skey->sk_strategy == InvalidStrategy)
-	{
-		/* Already-eliminated array scan key; don't need to fix anything */
-		Assert(skey->sk_flags & SK_SEARCHARRAY);
-		return true;
-	}
-
 	/* Adjust strategy for DESC, if we didn't already */
 	if ((addflags & SK_BT_DESC) && !(skey->sk_flags & SK_BT_DESC))
 		skey->sk_strategy = BTCommuteStrategyNumber(skey->sk_strategy);
@@ -3734,6 +5065,21 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/previous key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEG_INF | SK_BT_POS_INF |
+							 SK_BT_NEXTKEY | SK_BT_PREVKEY))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index edb09d4e3..e945686c8 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -96,6 +96,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 9c854e0e5..ea3d0f4b5 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,39 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	Assert(dexisting > DATEVAL_NOBEGIN);
+
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	Assert(dexisting < DATEVAL_NOEND);
+
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 date_finite(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index 8c6fc80c3..91682edd5 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -83,6 +83,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 5f5d7959d..33b1722df 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -6800,6 +6800,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		found_skip;
 	bool		found_saop;
 	bool		found_is_null_op;
 	double		num_sa_scans;
@@ -6825,6 +6826,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	found_skip = false;
 	found_saop = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
@@ -6833,15 +6835,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
 		ListCell   *lc2;
 
+		/*
+		 * XXX For now we just cost skip scans via generic rules: make a
+		 * uniform assumption that there will be 10 primitive index scans per
+		 * skipped attribute, relying on the "1/3 of all index pages" cap that
+		 * this costing has used since Postgres 17.  Also assume that skipping
+		 * won't take place for an index that has fewer than 100 pages.
+		 *
+		 * The current approach to costing leaves much to be desired, but is
+		 * at least better than nothing at all (keeping the code as it is on
+		 * HEAD just makes testing and review inconvenient).
+		 */
 		if (indexcol != iclause->indexcol)
 		{
 			/* Beginning of a new column's quals */
 			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			{
+				found_skip = true;	/* skip when no '=' qual for indexcol */
+				if (index->pages < 100)
+					break;
+				num_sa_scans += 10;
+			}
 			eqQualHere = false;
 			indexcol++;
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			{
+				/* no quals at all for indexcol */
+				found_skip = true;
+				if (index->pages < 100)
+					break;
+				num_sa_scans += 10 * (iclause->indexcol - indexcol);
+				continue;
+			}
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6914,6 +6939,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..9665e4985
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,54 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scans.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include <limits.h>
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		Datum		low_elem = sksup->low_elem;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->high_elem = low_elem;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 45eb1b2fe..a9222f896 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -390,6 +393,68 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	Assert(false);
+
+	return UUIDPGetDatum(uuid);
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	Assert(false);
+
+	return UUIDPGetDatum(uuid);
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 630ed0f16..6fc3ca1a7 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1702,6 +1703,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3525,6 +3537,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..9662fb2ba 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -583,6 +583,19 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure via an index <quote>skip scan</quote>.  The
+     APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 22d8ad1aa..f17dd3456 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1056,7 +1063,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal);
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1069,7 +1077,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal);
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1082,7 +1091,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal);
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..8b6b775c1 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 3bbe4c5f9..a8d5be6c1 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5138,9 +5138,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..4246afefd 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 635e6d6e2..58dec6a16 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2653,6 +2654,8 @@ SingleBoundSortItem
 SinglePartitionSpec
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

In reply to: Peter Geoghegan (#12)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Mon, Jul 15, 2024 at 2:34 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v3, which generalizes skip scan, allowing it to work with
opclasses/types that lack a skip support routine. In other words, v3
makes skip scan work for all types, including continuous types, where
it's impractical or infeasible to add skip support.

Attached is v4, which:

* Fixes a previous FIXME item affecting range skip scans/skip arrays
used in cross-type scenarios.

* Refactors and simplifies the handling of range inequalities
associated with skip arrays more generally. We now always use
inequality scan keys during array advancement (and when descending the
tree within _bt_first), rather than trying to use a datum taken from
the range inequality as an array element directly.

This gives us cleaner separation between scan keys/data types in
cross-type scenarios: skip arrays will now only ever contain
"elements" of opclass input type. Sentinel values such as -inf are
expanded to represent "the lowest possible value that comes after the
array's low_compare lower bound, if any". Opclasses that don't offer
skip support took roughly this same approach within v3, but in v4 all
opclasses do it the same way (so opclasses with skip support use the
SK_BT_NEG_INF sentinel marking in their scan keys, though never the
SK_BT_NEXTKEY sentinel marking).

This is really just a refactoring revision. Nothing particularly
exciting here compared to v3.

--
Peter Geoghegan

Attachments:

v4-0001-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v4-0001-Add-skip-scan-to-nbtree.patchDownload
From 3773fec62437d0f9a55d0484072b926acbfba001 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v4] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on an index (a, b) for queries with a predicate such as "WHERE b = 5".
This is useful in cases where the total number of distinct values in the
column 'a' is reasonably small (think hundreds, possibly thousands).

In effect, a skip scan treats the composite index on (a, b) as if it was
a series of disjunct subindexes -- one subindex per distinct 'a' value.
We exhaustively "search every subindex" using a qual that behaves like
"WHERE a = ANY(<every possible 'a' value>) AND b = 5".

The design of skip scan works by extended the design for arrays
established by commit 5bf748b8.  "Skip arrays" generate their array
values procedurally and on-demand, but otherwise work just like arrays
used by SAOPs.

B-Tree operator classes on discrete types can now optionally provide a
skip support routine.  This is used to generate the next array element
value by incrementing the current value (or by decrementing, in the case
of backwards scans).  When the opclass lacks a skip support routine, we
use sentinel next-key values instead.  Adding skip support makes skip
scans more efficient in cases where there is naturally a good chance
that the very next value will find matching tuples.  For example, during
an index scan with a leading "sales_date" attribute, there is a decent
chance that a scan that just finished returning tuples matching
"sales_date = '2024-06-01' and id = 5000" will find later tuples
matching "sales_date = '2024-06-02' and id = 5000".  It is to our
advantage to skip straight to the relevant "id = 5000" leaf page,
totally avoiding reading earlier "sales_date = '2024-06-02'" leaf pages.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/nbtree.h                 |   27 +-
 src/include/catalog/pg_amproc.dat           |   16 +
 src/include/catalog/pg_proc.dat             |   24 +
 src/include/utils/skipsupport.h             |  107 ++
 src/backend/access/nbtree/nbtcompare.c      |  261 +++
 src/backend/access/nbtree/nbtree.c          |   10 +-
 src/backend/access/nbtree/nbtsearch.c       |  111 +-
 src/backend/access/nbtree/nbtutils.c        | 1595 ++++++++++++++++---
 src/backend/access/nbtree/nbtvalidate.c     |    4 +
 src/backend/commands/opclasscmds.c          |   25 +
 src/backend/utils/adt/Makefile              |    1 +
 src/backend/utils/adt/date.c                |   44 +
 src/backend/utils/adt/meson.build           |    1 +
 src/backend/utils/adt/selfuncs.c            |   30 +-
 src/backend/utils/adt/skipsupport.c         |   52 +
 src/backend/utils/adt/uuid.c                |   67 +
 src/backend/utils/misc/guc_tables.c         |   23 +
 doc/src/sgml/btree.sgml                     |   13 +
 doc/src/sgml/xindex.sgml                    |   16 +-
 src/test/regress/expected/alter_generic.out |    6 +-
 src/test/regress/expected/psql.out          |    3 +-
 src/test/regress/sql/alter_generic.sql      |    2 +-
 src/tools/pgindent/typedefs.list            |    3 +
 23 files changed, 2237 insertions(+), 204 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 749304334..945091021 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1031,10 +1033,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard arrays that store elements in memory */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup set to valid routine? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* opclass skip scan support, when use_sksup */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo	order_low;		/* low_compare's ORDER proc */
+	FmgrInfo	order_high;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1123,6 +1137,11 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array, for skip scan */
+#define SK_BT_NEG_INF	0x00080000	/* -inf skip array element in sk_argument */
+#define SK_BT_POS_INF	0x00100000	/* +inf skip array element in sk_argument */
+#define SK_BT_NEXTKEY	0x00200000	/* interpret sk_argument as +infinitesimal */
+#define SK_BT_PREVKEY	0x00400000	/* interpret sk_argument as -infinitesimal */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1159,6 +1178,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index f639c3a6a..2a8f6f3f1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -122,6 +128,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +149,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +170,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +205,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +275,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 73d9cf858..27921e0df 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2214,6 +2229,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4368,6 +4386,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -9192,6 +9213,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..3d76c66b3
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,107 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * This happens at the point where the scan determines that another primitive
+ * index scan is required.  The next value is used (in combination with at
+ * least one additional lower-order non-skip key, taken from the SQL query) to
+ * relocate the scan, skipping over many irrelevant leaf pages in the process.
+ *
+ * Skip support generally works best with discrete types such as integer,
+ * date, and boolean; types where there is a decent chance that indexes will
+ * contain contiguous values (given a leading attributes using the opclass).
+ * When gaps/discontinuities are naturally rare (e.g., a leading identity
+ * column in a composite index, a date column preceding a product_id column),
+ * then it makes sense for skip scans to optimistically assume that the next
+ * distinct indexable value will find directly matching index tuples.
+ *
+ * The B-Tree code can fall back on next-key sentinel values for any opclass
+ * that doesn't provide its own skip support function.  There is no point in
+ * providing skip support unless the next indexed key value is often the next
+ * indexable value (at least with some workloads).  Opclasses where that never
+ * works out in practice should just rely on the B-Tree AM's generic next-key
+ * fallback strategy.  Opclasses where adding skip support is infeasible or
+ * hard (e.g., an opclass for a continuous type) can also use the fallback.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called.
+ * If an opclass can only set some of the fields, then it cannot safely
+ * provide a skip support routine.
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming standard ascending
+	 * order).  This helps the B-Tree code with finding its initial position
+	 * at the leaf level (during the skip scan's first primitive index scan).
+	 * In other words, it gives the B-Tree code a useful value to start from,
+	 * before any data has been read from the index.
+	 *
+	 * low_elem and high_elem are also used by skip scans to determine when
+	 * they've reached the final possible value (in the current direction).
+	 * It's typical for the scan to run out of leaf pages before it runs out
+	 * of unscanned indexable values, but it's still useful for the scan to
+	 * have a way to recognize when it has reached the last possible value
+	 * (this saves us a useless probe that just lands on the final leaf page).
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (in the case of pass-by-reference
+	 * types).  It's not okay for these functions to leak any memory.
+	 *
+	 * Both decrement and increment callbacks are guaranteed to never be
+	 * called with a NULL "existing" arg.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * *underflow (or set *overflow).  The return value is undefined when this
+	 * happens.  Opclass must not allocate memory for the undefined returned
+	 * value, since the B-Tree code isn't required to free the memory.
+	 *
+	 * The B-Tree skip scan caller's "existing" datum is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must be liberal
+	 * in accepting every possible representational variation within the
+	 * underlying data type.  On the other hand, opclasses are _not_ expected
+	 * to preserve any information that doesn't affect how datums are sorted
+	 * (e.g., skip support for a fixed precision numeric type isn't required
+	 * to preserve datum display scale).
+	 */
+	Datum		(*decrement) (Relation rel, Datum existing, bool *underflow);
+	Datum		(*increment) (Relation rel, Datum existing, bool *overflow);
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..deb387453 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,49 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +149,49 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +215,49 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +301,49 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +465,49 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +541,48 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 686a3206f..9c9cd48f7 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -324,11 +324,8 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	so = (BTScanOpaque) palloc(sizeof(BTScanOpaqueData));
 	BTScanPosInvalidate(so->currPos);
 	BTScanPosInvalidate(so->markPos);
-	if (scan->numberOfKeys > 0)
-		so->keyData = (ScanKey) palloc(scan->numberOfKeys * sizeof(ScanKeyData));
-	else
-		so->keyData = NULL;
 
+	so->keyData = NULL;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->arrayKeys = NULL;
@@ -408,6 +405,11 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 				scan->numberOfKeys * sizeof(ScanKeyData));
 	so->numberOfKeys = 0;		/* until _bt_preprocess_keys sets it */
 	so->numArrayKeys = 0;		/* ditto */
+
+	/* Release private storage allocated in previous btrescan, if any */
+	if (so->keyData != NULL)
+		pfree(so->keyData);
+	so->keyData = NULL;
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 57bcfc7e4..a78b69f88 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -880,7 +880,6 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	Buffer		buf;
 	BTStack		stack;
 	OffsetNumber offnum;
-	StrategyNumber strat;
 	BTScanInsertData inskey;
 	ScanKey		startKeys[INDEX_MAX_KEYS];
 	ScanKeyData notnullkeys[INDEX_MAX_KEYS];
@@ -1022,6 +1021,8 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		ScanKey		chosen;
 		ScanKey		impliesNN;
 		ScanKey		cur;
+		int			ikey = 0,
+					ichosen = 0;
 
 		/*
 		 * chosen is the so-far-chosen key for the current attribute, if any.
@@ -1042,6 +1043,80 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		{
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
+				/*
+				 * Conceptually, skip arrays consist of array elements whose
+				 * values are generated procedurally and on demand.  We need
+				 * special handling for that here.
+				 *
+				 * We must interpret various sentinel values to generate an
+				 * insertion scan key.  This is only actually needed for index
+				 * attributes whose input opclass lacks a skip support routine
+				 * (when skip support is available we'll always be able to
+				 * generate true array element datum values instead).
+				 */
+				if (chosen && chosen->sk_flags & (SK_BT_NEG_INF | SK_BT_POS_INF))
+				{
+					BTArrayKeyInfo *array = NULL;
+
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(!(chosen->sk_flags & (SK_BT_NEXTKEY | SK_BT_PREVKEY)));
+
+					for (; ikey < so->numArrayKeys; ikey++)
+					{
+						array = &so->arrayKeys[ikey];
+						if (array->scan_key == ichosen)
+							break;
+					}
+
+					Assert(array->scan_key == ichosen);
+					Assert(array->num_elems == -1);
+
+					if (array->null_elem)
+					{
+						/*
+						 * Treat the chosen scan key as having the value -inf
+						 * (or the value +inf, in the backwards scan case) by
+						 * not appending it to the local startKeys[] array.
+						 */
+						Assert(!array->low_compare);
+						Assert(!array->high_compare);
+						break;	/* done adding entries to startKeys[] */
+					}
+					else if ((chosen->sk_flags & SK_BT_NEG_INF) &&
+							 array->low_compare)
+					{
+						Assert(ScanDirectionIsForward(dir));
+
+						/* use array's inequality key in startKeys[] */
+						chosen = array->low_compare;
+					}
+					else if ((chosen->sk_flags & SK_BT_POS_INF) &&
+							 array->high_compare)
+					{
+						Assert(ScanDirectionIsBackward(dir));
+
+						/* use array's inequality key in startKeys[] */
+						chosen = array->high_compare;
+					}
+					else
+					{
+						/*
+						 * Array doesn't have any explicit low_compare or
+						 * high_compare that we can use (given the current
+						 * scan direction).  The array does not include a NULL
+						 * element (to generate an IS NULL qual), though, so
+						 * we might need to deduce a NOT NULL key to skip over
+						 * any NULLs.  Prepare for that.
+						 *
+						 * Note: this is also how we handle an explicit NOT
+						 * NULL key that preprocessing folded into the skip
+						 * array.
+						 */
+						impliesNN = chosen;
+						chosen = NULL;
+					}
+				}
+
 				/*
 				 * Done looking at keys for curattr.  If we didn't find a
 				 * usable boundary key, see if we can deduce a NOT NULL key.
@@ -1075,16 +1150,38 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 				startKeys[keysz++] = chosen;
 
+				/*
+				 * Skip arrays can also use a sk_argument which is marked
+				 * "next key".  This is another sentinel array element value
+				 * requiring special handling here by us.  As with -inf/+inf
+				 * sentinels, there cannot be any exact non-pivot matches.
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXTKEY | SK_BT_PREVKEY))
+				{
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(!(chosen->sk_flags & (SK_BT_NEG_INF | SK_BT_POS_INF)));
+					Assert(chosen->sk_strategy == BTEqualStrategyNumber);
+
+					/*
+					 * Adjust strat_total, so that our = key gets treated like
+					 * a > key (or like a < key)
+					 */
+					if (chosen->sk_flags & SK_BT_NEXTKEY)
+						strat_total = BTGreaterStrategyNumber;
+					else
+						strat_total = BTLessStrategyNumber;
+					break;
+				}
+
 				/*
 				 * Adjust strat_total, and quit if we have stored a > or <
 				 * key.
 				 */
-				strat = chosen->sk_strategy;
-				if (strat != BTEqualStrategyNumber)
+				if (chosen->sk_strategy != BTEqualStrategyNumber)
 				{
-					strat_total = strat;
-					if (strat == BTGreaterStrategyNumber ||
-						strat == BTLessStrategyNumber)
+					strat_total = chosen->sk_strategy;
+					if (chosen->sk_strategy == BTGreaterStrategyNumber ||
+						chosen->sk_strategy == BTLessStrategyNumber)
 						break;
 				}
 
@@ -1103,6 +1200,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 				curattr = cur->sk_attno;
 				chosen = NULL;
 				impliesNN = NULL;
+				ichosen = -1;
 			}
 
 			/*
@@ -1127,6 +1225,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 				case BTEqualStrategyNumber:
 					/* override any non-equality choice */
 					chosen = cur;
+					ichosen = i;
 					break;
 				case BTGreaterEqualStrategyNumber:
 				case BTGreaterStrategyNumber:
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index d6de2072d..5260a929a 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND c > 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c' (and so 'c' will still have an inequality scan key,
+ * required in only one direction -- 'c' won't be output as a "range" skip
+ * key/array).
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using opclass skip
+ * support routines.  This can be used to quantify the peformance benefit that
+ * comes from having dedicated skip support, with a given test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup set to valid routine? */
+	Oid			eq_op;			/* InvalidOid means don't skip */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -62,22 +90,49 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
-static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan);
+static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno,
+							BTSkipPreproc *skipatts);
+static inline Datum _bt_skipsupport_decrement(Relation rel, ScanKey skey,
+											  BTArrayKeyInfo *array, bool *underflow);
+static inline Datum _bt_skipsupport_increment(Relation rel, ScanKey skey,
+											  BTArrayKeyInfo *array, bool *overflow);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
-										   Datum arrdatum, ScanKey cur);
+										   Datum arrdatum, bool arrnull,
+										   ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 										 IndexTuple tuple, TupleDesc tupdesc, int tupnatts,
-										 bool readpagetup, int sktrig, bool *scanBehind);
+										 bool readpagetup, int sktrig, bool *scanBehind,
+										 bool infbefore);
 static bool _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 								   IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
 								   int sktrig, bool sktrig_required);
@@ -251,9 +306,6 @@ _bt_freestack(BTStack stack)
  * It is convenient for _bt_preprocess_keys caller to have to deal with no
  * more than one equality strategy array scan key per index attribute.  We'll
  * always be able to set things up that way when complete opfamilies are used.
- * Eliminated array scan keys can be recognized as those that have had their
- * sk_strategy field set to InvalidStrategy here by us.  Caller should avoid
- * including these in the scan's so->keyData[] output array.
  *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
@@ -261,18 +313,36 @@ _bt_freestack(BTStack stack)
  * preprocessing steps are complete.  This will convert the scan key offset
  * references into references to the scan's so->keyData[] output scan keys.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by later preprocessing on output.
+ * _bt_decide_skipatts decides which attributes receive skip arrays.
+ *
+ * Caller must pass *numberOfKeys to give us a way to change the number of
+ * input scan keys (our output is caller's input).  The returned array can be
+ * smaller than scan->keyData[] when we eliminated a redundant array scan key
+ * (redundant with some other array scan key, for the same attribute).  It can
+ * also be larger when we added a skip array/skip scan key.  Caller uses this
+ * to allocate so->keyData[] for the current btrescan.
+ *
  * Note: the reason we need to return a temp scan key array, rather than just
  * scribbling on scan->keyData, is that callers are permitted to call btrescan
  * without supplying a new set of scankey data.
  */
 static ScanKey
-_bt_preprocess_array_keys(IndexScanDesc scan)
+_bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
+	int			numArrayKeyData = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
-	int			numArrayKeys;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
+	int			numArrayKeys,
+				numSkipArrayKeys,
+				output_ikey = 0;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -280,11 +350,14 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
+	Assert(scan->numberOfKeys);
 
-	/* Quick check to see if there are any array keys */
+	/*
+	 * Quick check to see if there are any array keys, or any missing keys we
+	 * can generate a "skip scan" array key for ourselves
+	 */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
 		cur = &scan->keyData[i];
 		if (cur->sk_flags & SK_SEARCHARRAY)
@@ -300,6 +373,16 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		}
 	}
 
+	/* Consider generating skip arrays, and associated equality scan keys */
+	numSkipArrayKeys = _bt_decide_skipatts(scan, skipatts);
+	if (numSkipArrayKeys)
+	{
+		/* At least one skip array scan key must be added to arrayKeyData[] */
+		numArrayKeys += numSkipArrayKeys;
+		/* output scan key buffer allocation needs space for skip scan keys */
+		numArrayKeyData += numSkipArrayKeys;
+	}
+
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
@@ -317,19 +400,23 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
-	/* Create modifiable copy of scan->keyData in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
-	memcpy(arrayKeyData, scan->keyData, numberOfKeys * sizeof(ScanKeyData));
+	/* Create output scan keys in the workspace context */
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/*
+	 * Process each array key, and generate skip arrays as needed.  Also copy
+	 * every scan->keyData[] input scan key (whether it's an array or not)
+	 * into the arrayKeyData array we'll return to our caller (barring any
+	 * array scan keys that we could eliminate early through array merging).
+	 */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -345,14 +432,88 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		int			num_nonnulls;
 		int			j;
 
-		cur = &arrayKeyData[i];
-		if (!(cur->sk_flags & SK_SEARCHARRAY))
-			continue;
+		/* Create a skip array and scan key where indicated by skipatts */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* won't skip using this attribute */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[output_ikey];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * Temporary testing GUC can disable the use of an opclass's skip
+			 * support routine
+			 */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how the
+			 * consed-up "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[output_ikey], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			output_ikey++;		/* keep this scan key/array */
+		}
 
 		/*
-		 * First, deconstruct the array into elements.  Anything allocated
-		 * here (including a possibly detoasted array value) is in the
-		 * workspace context.
+		 * Copy input scan key into temp arrayKeyData scan key array.  (From
+		 * here on, cur points at our copy of the input scan key.)
+		 */
+		cur = &arrayKeyData[output_ikey];
+		*cur = scan->keyData[input_ikey];
+
+		if (!(cur->sk_flags & SK_SEARCHARRAY))
+		{
+			output_ikey++;		/* keep this non-array scan key */
+			continue;
+		}
+
+		/*
+		 * Deconstruct the array into elements
 		 */
 		arrayval = DatumGetArrayTypeP(cur->sk_argument);
 		/* We could cache this data, but not clear it's worth it */
@@ -406,6 +567,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
+				output_ikey++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -416,6 +578,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
+				output_ikey++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -432,7 +595,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[i], &sortprocp);
+							&so->orderProcs[output_ikey], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -476,11 +639,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					break;
 				}
 
-				/*
-				 * Indicate to _bt_preprocess_keys caller that it must ignore
-				 * this scan key
-				 */
-				cur->sk_strategy = InvalidStrategy;
+				/* Throw away this array */
 				continue;
 			}
 
@@ -511,12 +670,19 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = i;
+		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
 		numArrayKeys++;
+		output_ikey++;			/* keep this scan key/array */
 	}
 
+	/* Set final number of arrayKeyData[] keys, array keys */
+	*numberOfKeys = output_ikey;
 	so->numArrayKeys = numArrayKeys;
 
 	MemoryContextSwitchTo(oldContext);
@@ -624,7 +790,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -685,6 +852,253 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_decide_skipatts() -- set index attributes requiring skip arrays
+ *
+ * _bt_preprocess_array_keys helper function.  Determines which attributes
+ * will require skip arrays/scan keys.  Also sets up skip support callbacks
+ * for attributes whose input opclass have skip support (opclasses without
+ * skip support will fall back on using next-key sentinel values when
+ * advancing the skip array to its next array element).
+ *
+ * Return value is the total number of scan keys to add as "input" scan keys
+ * for further processing within _bt_preprocess_keys.
+ */
+static int
+_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts)
+{
+	Relation	rel = scan->indexRelation;
+	ScanKey		inputsk;
+	AttrNumber	attno_inputsk = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numSkipArrayKeys = 0,
+				prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * FIXME Don't support parallel index scans for now.
+	 *
+	 * _bt_parallel_primscan_schedule must be taught to account for skip
+	 * arrays. This is likely to require that we store the current array
+	 * element datum in shared memory.
+	 */
+	if (scan->parallel_scan)
+		return 0;
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan.
+	 */
+	inputsk = &scan->keyData[0];
+	for (int i = 0;; inputsk++, i++)
+	{
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inputsk
+		 */
+		while (attno_skip < attno_inputsk)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Opclass lacks a suitable skip support routine.
+				 *
+				 * Return prev_numSkipArrayKeys, so as to avoid including any
+				 * "backfilled" arrays that were supposed to form a contiguous
+				 * group with a skip array on this attribute.  There is no
+				 * benefit to adding backfill skip arrays unless we can do so
+				 * for all attributes (all attributes up to and including the
+				 * one immediately before attno_inputsk).
+				 */
+				return prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			numSkipArrayKeys++;
+			attno_skip++;
+		}
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i >= scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 * (either in part or in whole)
+		 */
+		if (attno_inputsk > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inputsk (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inputsk < inputsk->sk_attno)
+		{
+			prev_numSkipArrayKeys = numSkipArrayKeys;
+
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (!attno_has_equal)
+			{
+				/* Only saw inequalities for the prior attribute */
+				if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+				{
+					/* add a range skip array for this attribute */
+					numSkipArrayKeys++;
+				}
+				else
+					break;
+			}
+			else
+			{
+				/*
+				 * Saw an equality for the prior attribute, so it doesn't need
+				 * a skip array (not even a range skip array).  We'll be able
+				 * to add later skip arrays, too (doesn't matter if the prior
+				 * attribute uses an input opclass without skip support).
+				 */
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inputsk = inputsk->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this scan key's attribute has any equality strategy scan
+		 * keys.
+		 *
+		 * Treat IS NULL scan keys as using equal strategy (they'll be marked
+		 * as using it later on, by _bt_fix_scankey_strategy).
+		 */
+		if (inputsk->sk_strategy == BTEqualStrategyNumber ||
+			(inputsk->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.
+		 *
+		 * We do still backfill skip attributes before the RowCompare, so that
+		 * it can be marked required.  This is similar to what happens when a
+		 * conventional inequality uses an opclass that lacks skip support.
+		 */
+		if (inputsk->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	return numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatts
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false.
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatts)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatts->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										  BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect input opclasses lacking even an equality
+	 * operator, but they're still supported.  Deal with them gracefully.
+	 */
+	if (!OidIsValid(skipatts->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatts->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+														reverse,
+														&skipatts->sksup);
+
+	/* might not have set up skip support routine, but can skip either way */
+	return true;
+}
+
+/*
+ * _bt_skipsupport_decrement() -- Get a decremented copy of skey's arg
+ *
+ * Sets *underflow for caller.  Returns a valid decremented value (allocated
+ * in caller's memory context for pass-by-reference types) when *underflow is
+ * set to 'false'.  Otherwise returns an undefined value that caller doesn't
+ * have to pfree.
+ */
+static inline Datum
+_bt_skipsupport_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  bool *underflow)
+{
+	Assert(array->use_sksup);
+
+	if (!(skey->sk_flags & SK_BT_DESC))
+		return array->sksup.decrement(rel, skey->sk_argument, underflow);
+	else
+		return array->sksup.increment(rel, skey->sk_argument, underflow);
+}
+
+/*
+ * _bt_skipsupport_increment() -- Get an incremented copy of skey's arg
+ *
+ * Sets *overflow for caller.  Returns a valid incremented value (allocated in
+ * caller's memory context for pass-by-reference types) when *overflow is set
+ * to 'false'.  Otherwise returns an undefined value that caller doesn't have
+ * to pfree.
+ */
+static inline Datum
+_bt_skipsupport_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  bool *overflow)
+{
+	Assert(array->use_sksup);
+
+	if (!(skey->sk_flags & SK_BT_DESC))
+		return array->sksup.increment(rel, skey->sk_argument, overflow);
+	else
+		return array->sksup.decrement(rel, skey->sk_argument, overflow);
+}
+
 /*
  * _bt_setup_array_cmp() -- Set up array comparison functions
  *
@@ -977,17 +1391,15 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
@@ -1000,8 +1412,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.  We have to be sure
+	 * that _bt_compare_array_skey/_bt_binsrch_array_skey use the right proc.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1032,11 +1444,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensured that the scalar scan key could be eliminated as redundant
+		 */
+		eliminated = true;
+	}
+	else
+	{
+		/*
+		 * With a skip array it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when earlier preprocessing wasn't able to eliminate a
+		 * redundant scan key inequality due to a lack of cross-type support.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when it
+ * is redundant with (or contradicted by) a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1088,6 +1554,137 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of skip array scan key when it is "redundant with"
+ * a non-array scalar scan key.  The scalar scan key must be an inequality.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array, allowing its elements to be generated within the limits of a range.
+ * Calling here always renders caller's scalar scan key redundant (the key is
+ * applied when the array advances, but that's just an implementation detail).
+ *
+ * Return value indicates if the array already had a lower/upper bound
+ * (whichever caller's scalar scan key was expected to be).  We return true in
+ * the common case where caller's scan key could be successfully rolled into
+ * the skip array.  We return false when we can't do that due to the presence
+ * of a conflicting inequality.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	/*
+	 * We don't expect to have to deal with NULLs in non-array/non-skip scan
+	 * key.  We expect _bt_preprocess_array_keys to avoid generating a skip
+	 * array for an index attribute with an IS NULL input scan key.  (It will
+	 * still do so in the presence of IS NOT NULL input scan keys, but
+	 * _bt_compare_scankey_args is expected to handle those for us.)
+	 */
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(arraysk->sk_flags & SK_SEARCHARRAY);
+	Assert(arraysk->sk_strategy == BTEqualStrategyNumber);
+	Assert(array->num_elems == -1);
+
+	/* Scalar scan key must be a B-Tree inequality, which are always strict */
+	Assert(!(skey->sk_flags & SK_ISNULL));
+	Assert(skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * A skip array scan key always uses the underlying index attribute's
+	 * input opclass, but it's possible that caller's scalar scan key uses a
+	 * cross-type operator.  In cross-type scenarios, skey.sk_argument doesn't
+	 * use the same type as later array elements (which are all just copies of
+	 * datums taken from index tuples, possibly modified by skip support).
+	 *
+	 * We represent the lowest (and highest) possible value in the array using
+	 * the sentinel value -inf (+inf for high_compare).  The only exceptions
+	 * apply when the opclass has skip support: there we can use a copy of the
+	 * skip support routine's low_elem/high_elem instead -- though only when
+	 * there is no corresponding low_compare/high_compare inequality.
+	 *
+	 * _bt_first understands that -inf/+inf indicate that it should use the
+	 * low_compare/high_compare inequality for initial positioning purposes
+	 * when it sees either value (unless there is no corresponding inequality,
+	 * in which case the values are literally interpreted as -inf or +inf).
+	 * _bt_first can therefore vary in whether it uses a cross-type operator,
+	 * or an input-opclass-only operator (it can vary across primitive scans
+	 * for the same index attribute/skip array).
+	 *
+	 * _bt_scankey_decrement/_bt_scankey_increment both make sure that each
+	 * newly generated element is constrained by low_compare/high_compare.
+	 * This must happen without skey.sk_argument ever being treated as a true
+	 * array element (that wouldn't always work because array elements are
+	 * only ever supposed to use the opclass input type).
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare */
+
+				/* replace old high_compare with new one */
+			}
+			else
+				array->high_compare = palloc(sizeof(ScanKeyData));
+
+			memcpy(array->high_compare, skey, sizeof(ScanKeyData));
+			array->order_high = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare */
+
+				/* replace old low_compare with new one */
+			}
+			else
+				array->low_compare = palloc(sizeof(ScanKeyData));
+
+			memcpy(array->low_compare, skey, sizeof(ScanKeyData));
+			array->order_low = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
@@ -1130,7 +1727,8 @@ _bt_compare_array_elements(const void *a, const void *b, void *arg)
 static inline int32
 _bt_compare_array_skey(FmgrInfo *orderproc,
 					   Datum tupdatum, bool tupnull,
-					   Datum arrdatum, ScanKey cur)
+					   Datum arrdatum, bool arrnull,
+					   ScanKey cur)
 {
 	int32		result = 0;
 
@@ -1138,14 +1736,14 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 
 	if (tupnull)				/* NULL tupdatum */
 	{
-		if (cur->sk_flags & SK_ISNULL)
+		if (arrnull)
 			result = 0;			/* NULL "=" NULL */
 		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = -1;		/* NULL "<" NOT_NULL */
 		else
 			result = 1;			/* NULL ">" NOT_NULL */
 	}
-	else if (cur->sk_flags & SK_ISNULL) /* NOT_NULL tupdatum, NULL arrdatum */
+	else if (arrnull)			/* NOT_NULL tupdatum, NULL arrdatum */
 	{
 		if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = 1;			/* NOT_NULL ">" NULL */
@@ -1211,6 +1809,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1246,7 +1846,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[low_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result <= 0)
 				{
@@ -1274,7 +1874,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[high_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result >= 0)
 				{
@@ -1301,7 +1901,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 		arrdatum = array->elem_values[mid_elem];
 
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										arrdatum, cur);
+										arrdatum, false, cur);
 
 		if (result == 0)
 		{
@@ -1326,13 +1926,70 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	 */
 	if (low_elem != mid_elem)
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										array->elem_values[low_elem], cur);
+										array->elem_values[low_elem], false,
+										cur);
 
 	*set_elem_result = result;
 
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * This routine doesn't return an index into the array, because the array
+ * doesn't actually have any elements (it generates its array elements
+ * procedurally instead).  Note that this may include a NULL value/an IS NULL
+ * qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc, Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										tupdatum,
+										array->low_compare->sk_argument)))
+		*set_elem_result = -1;
+	else if (array->high_compare &&
+			 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											 array->high_compare->sk_collation,
+											 tupdatum,
+											 array->high_compare->sk_argument)))
+		*set_elem_result = 1;
+	else
+		*set_elem_result = 0;
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1342,29 +1999,498 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
 		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
 		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, curArrayKey,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = false;
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Clear possibly-irrelevant flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEG_INF | SK_BT_POS_INF |
+						SK_BT_NEXTKEY | SK_BT_PREVKEY);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Lowest (or highest) element is NULL, so set scan key to NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Lowest array element isn't NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEG_INF;
+	}
+	else
+	{
+		/* Highest array element isn't NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_POS_INF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEG_INF | SK_BT_POS_INF |
+						SK_BT_NEXTKEY | SK_BT_PREVKEY);
+
+	/*
+	 * Treat tupdatum/tupnull as a matching array element.
+	 *
+	 * We just copy tupdatum into the array's scan key (there is no
+	 * conventional array element for us to set, of course).
+	 *
+	 * Unlike standard arrays, skip arrays sometimes need to locate NULLs.
+	 * Treat them as just another value from the domain of indexed values.
+	 */
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEG_INF | SK_BT_POS_INF |
+							   SK_BT_NEXTKEY | SK_BT_PREVKEY)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEG_INF | SK_BT_POS_INF |
+						SK_BT_NEXTKEY | SK_BT_PREVKEY);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEG_INF | SK_BT_POS_INF |
+							   SK_BT_NEXTKEY | SK_BT_PREVKEY)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		underflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_POS_INF | SK_BT_NEXTKEY | SK_BT_PREVKEY)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEG_INF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PREVKEY flag.  The true previous value can only
+	 * be determined when the scan reads lower sorting tuples.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the previous element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * low_compare is for an >= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true previous value
+		 * cannot possibly satisfy low_compare.  We can give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(&array->order_low,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, false,
+								   skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true previous value */
+		skey->sk_flags |= SK_BT_PREVKEY;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Decrement" current array element to the high_elem value provided
+		 * by opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = _bt_skipsupport_decrement(rel, skey, array, &underflow);
+
+	if (underflow)
+	{
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/*
+			 * Existing sk_argument was already equal to non-NULL low_elem
+			 * provided by opclass skip support routine, but skip array's true
+			 * lowest element is actually NULL.
+			 *
+			 * "Decrement" sk_argument to NULL.
+			 */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Make sure that the decremented value is within the range of the skip
+	 * array
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* decremented value is out of bounds for range skip array */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		overflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEG_INF | SK_BT_NEXTKEY | SK_BT_PREVKEY)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_POS_INF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXTKEY flag.  The true previous value can only
+	 * be determined when the scan reads higher sorting tuples.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * high_compare is for an <= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true next value cannot
+		 * possibly satisfy high_compare.  We can give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(&array->order_high,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, false,
+								   skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true next value */
+		skey->sk_flags |= SK_BT_NEXTKEY;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Increment" current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = _bt_skipsupport_increment(rel, skey, array, &overflow);
+
+	if (overflow)
+	{
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/*
+			 * Existing sk_argument was already equal to non-NULL high_elem
+			 * provided by opclass skip support routine, but skip array's true
+			 * highest element is actually NULL.
+			 *
+			 * "Decrement" sk_argument to NULL.
+			 */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Make sure that the incremented value is within the range of the skip
+	 * array
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* incremented value is out of bounds for range skip array */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1380,6 +2506,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1389,29 +2516,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then advance next most significant array, if any */
 	}
 
 	/*
@@ -1466,6 +2594,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1473,7 +2602,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1485,16 +2613,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No skipping of non-required arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1539,11 +2661,22 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
  * the page to the right of caller's finaltup/high key tuple instead).  It's
  * only possible that we'll set *scanBehind to true when caller passes us a
  * pivot tuple (with truncated -inf attributes) that we return false for.
+ *
+ * When a skip array sets its scan key to -inf (or to +inf in the case of a
+ * backwards scan), the tuple will never be before the scan's current array
+ * keys on the basis of that particular scan key/tuple attribute value.
+ * However, some caller's (infbefore callers) need us to resolve such a
+ * comparison by treating the -inf/+inf value as coming before every other
+ * value instead (before relative to the current scan direction).  This scheme
+ * allows _bt_advance_array_keys to schedule the next primitive index scan
+ * when the page's finaltup has no values within the range of a range skip
+ * array, iff no earlier scan key triggered the next primitive scan first.
  */
 static bool
 _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 							 IndexTuple tuple, TupleDesc tupdesc, int tupnatts,
-							 bool readpagetup, int sktrig, bool *scanBehind)
+							 bool readpagetup, int sktrig, bool *scanBehind,
+							 bool infbefore)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
@@ -1558,6 +2691,8 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 	for (int ikey = sktrig; ikey < so->numberOfKeys; ikey++)
 	{
 		ScanKey		cur = so->keyData + ikey;
+		Datum		sk_argument = cur->sk_argument;
+		bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
 		Datum		tupdatum;
 		bool		tupnull;
 		int32		result;
@@ -1617,11 +2752,27 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * When scan key is marked NEG_INF, the current array element is lower
+		 * than any possible indexable value (or it's lower than any possible
+		 * value that satisfies the array's low_compare > or >= inequality).
+		 *
+		 * Similarly, when scan key is marked POS_INF, the current element is
+		 * higher than any possible indexable value (or it's higher than any
+		 * value satisfying the array's high_compare < or <= inequality).
+		 */
+		if (cur->sk_flags & (SK_BT_NEG_INF | SK_BT_POS_INF))
+		{
+			Assert(cur->sk_flags & SK_BT_SKIP);
+			Assert(cur->sk_argument == 0);
+			return infbefore;
+		}
+
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
 		result = _bt_compare_array_skey(&so->orderProcs[ikey],
 										tupdatum, tupnull,
-										cur->sk_argument, cur);
+										sk_argument, sk_isnull, cur);
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1631,6 +2782,19 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 			(ScanDirectionIsBackward(dir) && result > 0))
 			return true;
 
+		/*
+		 * When scan key is marked NEXTKEY, the current array element is
+		 * "sk_argument + infinitesimal" (with PREVKEY the current element is
+		 * "sk_argument - infinitesimal" instead).  In other words, its value
+		 * comes immediately after (or immediately before) sk_argument in the
+		 * key space.
+		 */
+		if ((cur->sk_flags & (SK_BT_NEXTKEY | SK_BT_PREVKEY)) && result == 0)
+		{
+			Assert(cur->sk_flags & SK_BT_SKIP);
+			return true;
+		}
+
 		/*
 		 * Does this comparison indicate that caller should now advance the
 		 * scan's arrays?  (Must be if we get here during a readpagetup call.)
@@ -1806,7 +2970,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * Precondition array state assertion
 		 */
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
-											 tupnatts, false, 0, NULL));
+											 tupnatts, false, 0, NULL, false));
 
 		so->scanBehind = false; /* reset */
 
@@ -1954,18 +3118,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1990,18 +3145,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2019,15 +3165,26 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * Skip array.  "Binary search" by checking if tupdatum/tupnull
+			 * are within the low_value/high_value range of the skip array.
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
+			Datum		sk_argument = cur->sk_argument;
+			bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
+
 			Assert(sktrig_required && required);
 
 			/*
@@ -2041,7 +3198,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 */
 			result = _bt_compare_array_skey(&so->orderProcs[ikey],
 											tupdatum, tupnull,
-											cur->sk_argument, cur);
+											sk_argument, sk_isnull, cur);
 		}
 
 		/*
@@ -2100,11 +3257,62 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+
+		if (!array)
+			continue;			/* no element to set in non-array */
+
+		/* Conventional arrays have a valid set_elem for us to advance to */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * Conceptually, skip arrays also have array elements.  The actual
+		 * elements/values are generated procedurally and on demand.
+		 */
+		Assert(cur->sk_flags & SK_BT_SKIP);
+		Assert(array->num_elems == -1);
+		Assert(required);
+
+		if (result == 0)
+		{
+			/*
+			 * Anything within the range of possible element values is treated
+			 * as "a match for one of the array's elements".  Store the next
+			 * scan key argument value by taking a copy of the tupdatum value
+			 * from caller's tuple (or set scan key IS NULL when tupnull, iff
+			 * the array's range of possible elements covers NULL).
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
+		}
+		else if (beyond_end_advance)
+		{
+			/*
+			 * We need to set the array element to the final "element" in the
+			 * current scan direction for "beyond end of array element" array
+			 * advancement.  See above for an explanation.
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else
+		{
+			/*
+			 * The closest matching element is the lowest element; even that
+			 * still puts us ahead of caller's tuple in the key space.  This
+			 * process has to carry to any lower-order arrays.  See above for
+			 * an explanation.
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
 		}
 	}
 
@@ -2234,7 +3442,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * scan direction to deal with NULLs.  We'll account for that separately.)
 	 */
 	Assert(_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts,
-										false, 0, NULL) ==
+										false, 0, NULL, true) ==
 		   !all_required_satisfied);
 
 	/*
@@ -2259,7 +3467,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	if (!all_required_satisfied && pstate->finaltup &&
 		_bt_tuple_before_array_skeys(scan, dir, pstate->finaltup, tupdesc,
 									 BTreeTupleGetNAtts(pstate->finaltup, rel),
-									 false, 0, &so->scanBehind))
+									 false, 0, &so->scanBehind, true))
 		goto new_prim_scan;
 
 	/*
@@ -2460,10 +3668,12 @@ end_toplevel_scan:
 /*
  *	_bt_preprocess_keys() -- Preprocess scan keys
  *
+ * The first call here (per btrescan) allocates so->keyData[].
  * The given search-type keys (taken from scan->keyData[])
  * are copied to so->keyData[] with possible transformation.
  * scan->numberOfKeys is the number of input keys, so->numberOfKeys gets
- * the number of output keys (possibly less, never greater).
+ * the number of output keys.  Calling here a second or subsequent time
+ * (during the same btrescan) is a no-op.
  *
  * The output keys are marked with additional sk_flags bits beyond the
  * system-standard bits supplied by the caller.  The DESC and NULLS_FIRST
@@ -2483,6 +3693,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also cons up skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2550,9 +3762,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	int16	   *indoption = scan->indexRelation->rd_indoption;
 	int			new_numberOfKeys;
 	int			numberOfEqualCols;
-	ScanKey		inkeys;
-	ScanKey		outkeys;
-	ScanKey		cur;
+	ScanKey		inputsk;
 	BTScanKeyPreproc xform[BTMaxStrategyNumber];
 	bool		test_result;
 	int			i,
@@ -2584,7 +3794,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		return;					/* done if qual-less scan */
 
 	/* If any keys are SK_SEARCHARRAY type, set up array-key info */
-	arrayKeyData = _bt_preprocess_array_keys(scan);
+	arrayKeyData = _bt_preprocess_array_keys(scan, &numberOfKeys);
 	if (!so->qual_ok)
 	{
 		/* unmatchable array, so give up */
@@ -2598,32 +3808,36 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	 */
 	if (arrayKeyData)
 	{
-		inkeys = arrayKeyData;
+		inputsk = arrayKeyData;
 
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
 	}
 	else
-		inkeys = scan->keyData;
+		inputsk = scan->keyData;
+
+	/*
+	 * Now that we have an estimate of the number of output scan keys
+	 * (including any skip array scan keys), allocate space for them
+	 */
+	so->keyData = palloc(sizeof(ScanKeyData) * numberOfKeys);
 
-	outkeys = so->keyData;
-	cur = &inkeys[0];
 	/* we check that input keys are correctly ordered */
-	if (cur->sk_attno < 1)
+	if (inputsk->sk_attno < 1)
 		elog(ERROR, "btree index keys must be ordered by attribute");
 
 	/* We can short-circuit most of the work if there's just one key */
 	if (numberOfKeys == 1)
 	{
 		/* Apply indoption to scankey (might change sk_strategy!) */
-		if (!_bt_fix_scankey_strategy(cur, indoption))
+		if (!_bt_fix_scankey_strategy(inputsk, indoption))
 			so->qual_ok = false;
-		memcpy(outkeys, cur, sizeof(ScanKeyData));
+		memcpy(&so->keyData[0], inputsk, sizeof(ScanKeyData));
 		so->numberOfKeys = 1;
 		/* We can mark the qual as required if it's for first index col */
-		if (cur->sk_attno == 1)
-			_bt_mark_scankey_required(outkeys);
+		if (inputsk->sk_attno == 1)
+			_bt_mark_scankey_required(&so->keyData[0]);
 		if (arrayKeyData)
 		{
 			/*
@@ -2631,8 +3845,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * (we'll miss out on the single value array transformation, but
 			 * that's not nearly as important when there's only one scan key)
 			 */
-			Assert(cur->sk_flags & SK_SEARCHARRAY);
-			Assert(cur->sk_strategy != BTEqualStrategyNumber ||
+			Assert(so->keyData[0].sk_flags & SK_SEARCHARRAY);
+			Assert(so->keyData[0].sk_strategy != BTEqualStrategyNumber ||
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
@@ -2660,12 +3874,12 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	 * handle after-last-key processing.  Actual exit from the loop is at the
 	 * "break" statement below.
 	 */
-	for (i = 0;; cur++, i++)
+	for (i = 0;; inputsk++, i++)
 	{
 		if (i < numberOfKeys)
 		{
 			/* Apply indoption to scankey (might change sk_strategy!) */
-			if (!_bt_fix_scankey_strategy(cur, indoption))
+			if (!_bt_fix_scankey_strategy(inputsk, indoption))
 			{
 				/* NULL can't be matched, so give up */
 				so->qual_ok = false;
@@ -2677,12 +3891,12 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		 * If we are at the end of the keys for a particular attr, finish up
 		 * processing and emit the cleaned-up keys.
 		 */
-		if (i == numberOfKeys || cur->sk_attno != attno)
+		if (i == numberOfKeys || inputsk->sk_attno != attno)
 		{
 			int			priorNumberOfEqualCols = numberOfEqualCols;
 
 			/* check input keys are correctly ordered */
-			if (i < numberOfKeys && cur->sk_attno < attno)
+			if (i < numberOfKeys && inputsk->sk_attno < attno)
 				elog(ERROR, "btree index keys must be ordered by attribute");
 
 			/*
@@ -2741,7 +3955,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
+						Assert(!array || array->num_elems > 0 ||
+							   array->num_elems == -1);
 						xform[j].skey = NULL;
 						xform[j].ikey = -1;
 					}
@@ -2786,7 +4001,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			}
 
 			/*
-			 * Emit the cleaned-up keys into the outkeys[] array, and then
+			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
 			 */
@@ -2794,7 +4009,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			{
 				if (xform[j].skey)
 				{
-					ScanKey		outkey = &outkeys[new_numberOfKeys++];
+					ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
 					memcpy(outkey, xform[j].skey, sizeof(ScanKeyData));
 					if (arrayKeyData)
@@ -2811,19 +4026,19 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				break;
 
 			/* Re-initialize for new attno */
-			attno = cur->sk_attno;
+			attno = inputsk->sk_attno;
 			memset(xform, 0, sizeof(xform));
 		}
 
 		/* check strategy this key's operator corresponds to */
-		j = cur->sk_strategy - 1;
+		j = inputsk->sk_strategy - 1;
 
 		/* if row comparison, push it directly to the output array */
-		if (cur->sk_flags & SK_ROW_HEADER)
+		if (inputsk->sk_flags & SK_ROW_HEADER)
 		{
-			ScanKey		outkey = &outkeys[new_numberOfKeys++];
+			ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
-			memcpy(outkey, cur, sizeof(ScanKeyData));
+			memcpy(outkey, inputsk, sizeof(ScanKeyData));
 			if (arrayKeyData)
 				keyDataMap[new_numberOfKeys - 1] = i;
 			if (numberOfEqualCols == attno - 1)
@@ -2837,19 +4052,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			continue;
 		}
 
-		/*
-		 * Does this input scan key require further processing as an array?
-		 */
-		if (cur->sk_strategy == InvalidStrategy)
-		{
-			/* _bt_preprocess_array_keys marked this array key redundant */
-			Assert(arrayKeyData);
-			Assert(cur->sk_flags & SK_SEARCHARRAY);
-			continue;
-		}
-
-		if (cur->sk_strategy == BTEqualStrategyNumber &&
-			(cur->sk_flags & SK_SEARCHARRAY))
+		if (inputsk->sk_strategy == BTEqualStrategyNumber &&
+			(inputsk->sk_flags & SK_SEARCHARRAY))
 		{
 			/* _bt_preprocess_array_keys kept this array key */
 			Assert(arrayKeyData);
@@ -2863,7 +4067,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		if (xform[j].skey == NULL)
 		{
 			/* nope, so this scan key wins by default (at least for now) */
-			xform[j].skey = cur;
+			xform[j].skey = inputsk;
 			xform[j].ikey = i;
 			xform[j].arrayidx = arrayidx;
 		}
@@ -2881,7 +4085,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/*
 				 * Have to set up array keys
 				 */
-				if ((cur->sk_flags & SK_SEARCHARRAY))
+				if (inputsk->sk_flags & SK_SEARCHARRAY)
 				{
 					array = &so->arrayKeys[arrayidx - 1];
 					orderproc = so->orderProcs + i;
@@ -2909,13 +4113,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				 */
 			}
 
-			if (_bt_compare_scankey_args(scan, cur, cur, xform[j].skey,
+			if (_bt_compare_scankey_args(scan, inputsk, inputsk, xform[j].skey,
 										 array, orderproc, &test_result))
 			{
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
+					Assert(!array || array->num_elems > 0 ||
+						   array->num_elems == -1);
 
 					/*
 					 * New key is more restrictive, and so replaces old key...
@@ -2923,7 +4128,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 					if (j != (BTEqualStrategyNumber - 1) ||
 						!(xform[j].skey->sk_flags & SK_SEARCHARRAY))
 					{
-						xform[j].skey = cur;
+						xform[j].skey = inputsk;
 						xform[j].ikey = i;
 						xform[j].arrayidx = arrayidx;
 					}
@@ -2936,7 +4141,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 						 * scan key.  _bt_compare_scankey_args expects us to
 						 * always keep arrays (and discard non-arrays).
 						 */
-						Assert(!(cur->sk_flags & SK_SEARCHARRAY));
+						Assert(!(inputsk->sk_flags & SK_SEARCHARRAY));
 					}
 				}
 				else if (j == (BTEqualStrategyNumber - 1))
@@ -2959,14 +4164,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				 * even with incomplete opfamilies.  _bt_advance_array_keys
 				 * depends on this.
 				 */
-				ScanKey		outkey = &outkeys[new_numberOfKeys++];
+				ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
 				memcpy(outkey, xform[j].skey, sizeof(ScanKeyData));
 				if (arrayKeyData)
 					keyDataMap[new_numberOfKeys - 1] = xform[j].ikey;
 				if (numberOfEqualCols == attno - 1)
 					_bt_mark_scankey_required(outkey);
-				xform[j].skey = cur;
+				xform[j].skey = inputsk;
 				xform[j].ikey = i;
 				xform[j].arrayidx = arrayidx;
 			}
@@ -3057,10 +4262,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3135,6 +4341,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3208,6 +4430,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3380,13 +4603,6 @@ _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption)
 		return true;
 	}
 
-	if (skey->sk_strategy == InvalidStrategy)
-	{
-		/* Already-eliminated array scan key; don't need to fix anything */
-		Assert(skey->sk_flags & SK_SEARCHARRAY);
-		return true;
-	}
-
 	/* Adjust strategy for DESC, if we didn't already */
 	if ((addflags & SK_BT_DESC) && !(skey->sk_flags & SK_BT_DESC))
 		skey->sk_strategy = BTCommuteStrategyNumber(skey->sk_strategy);
@@ -3524,7 +4740,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 */
 		Assert(!so->scanBehind && !pstate->prechecked && !pstate->firstmatch);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
-											 tupnatts, false, 0, NULL));
+											 tupnatts, false, 0, NULL, false));
 	}
 	if (pstate->prechecked || pstate->firstmatch)
 	{
@@ -3560,7 +4776,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
-									 ikey, NULL))
+									 ikey, NULL, false))
 	{
 		/*
 		 * Tuple is still before the start of matches according to the scan's
@@ -3579,7 +4795,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 			_bt_tuple_before_array_skeys(scan, dir, pstate->finaltup, tupdesc,
 										 BTreeTupleGetNAtts(pstate->finaltup,
 															scan->indexRelation),
-										 false, 0, NULL))
+										 false, 0, NULL, false))
 		{
 			/* Cut our losses -- start a new primitive index scan now */
 			pstate->continuescan = false;
@@ -3734,6 +4950,21 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/previous key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEG_INF | SK_BT_POS_INF |
+							 SK_BT_NEXTKEY | SK_BT_PREVKEY))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
@@ -4105,7 +5336,7 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	ahead = (IndexTuple) PageGetItem(pstate->page,
 									 PageGetItemId(pstate->page, aheadoffnum));
 	if (_bt_tuple_before_array_skeys(scan, dir, ahead, tupdesc, tupnatts,
-									 false, 0, NULL))
+									 false, 0, NULL, false))
 	{
 		/*
 		 * Success -- instruct _bt_readpage to skip ahead to very next tuple
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index edb09d4e3..e945686c8 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -96,6 +96,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 9c854e0e5..79658f068 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,49 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 date_finite(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index 8c6fc80c3..91682edd5 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -83,6 +83,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 5f5d7959d..33b1722df 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -6800,6 +6800,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		found_skip;
 	bool		found_saop;
 	bool		found_is_null_op;
 	double		num_sa_scans;
@@ -6825,6 +6826,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	found_skip = false;
 	found_saop = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
@@ -6833,15 +6835,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
 		ListCell   *lc2;
 
+		/*
+		 * XXX For now we just cost skip scans via generic rules: make a
+		 * uniform assumption that there will be 10 primitive index scans per
+		 * skipped attribute, relying on the "1/3 of all index pages" cap that
+		 * this costing has used since Postgres 17.  Also assume that skipping
+		 * won't take place for an index that has fewer than 100 pages.
+		 *
+		 * The current approach to costing leaves much to be desired, but is
+		 * at least better than nothing at all (keeping the code as it is on
+		 * HEAD just makes testing and review inconvenient).
+		 */
 		if (indexcol != iclause->indexcol)
 		{
 			/* Beginning of a new column's quals */
 			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			{
+				found_skip = true;	/* skip when no '=' qual for indexcol */
+				if (index->pages < 100)
+					break;
+				num_sa_scans += 10;
+			}
 			eqQualHere = false;
 			indexcol++;
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			{
+				/* no quals at all for indexcol */
+				found_skip = true;
+				if (index->pages < 100)
+					break;
+				num_sa_scans += 10 * (iclause->indexcol - indexcol);
+				continue;
+			}
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6914,6 +6939,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..796e998a9
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,52 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		Datum		low_elem = sksup->low_elem;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->high_elem = low_elem;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 45eb1b2fe..e2d98a62f 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -390,6 +393,70 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	*underflow = false;
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	*underflow = true;
+
+	return 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	*overflow = false;
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	*overflow = true;
+
+	return 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 630ed0f16..6fc3ca1a7 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1702,6 +1703,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3525,6 +3537,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..9662fb2ba 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -583,6 +583,19 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure via an index <quote>skip scan</quote>.  The
+     APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 22d8ad1aa..f17dd3456 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1056,7 +1063,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal);
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1069,7 +1077,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal);
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1082,7 +1091,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal);
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..8b6b775c1 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 3bbe4c5f9..a8d5be6c1 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5138,9 +5138,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..4246afefd 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b4d7f9217..b5b5c2494 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2654,6 +2655,8 @@ SingleBoundSortItem
 SinglePartitionSpec
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

#14Dmitry Dolgov
9erthalion6@gmail.com
In reply to: Peter Geoghegan (#12)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Jun 26, 2024 at 03:16:07PM GMT, Peter Geoghegan wrote:

Loose index scan is a far more specialized technique than skip scan.
It only applies within special scans that feed into a DISTINCT group
aggregate. Whereas my skip scan patch isn't like that at all -- it's
much more general. With my patch, nbtree has exactly the same contract
with the executor/core code as before. There are no new index paths
generated by the optimizer to make skip scan work, even. Skip scan
isn't particularly aimed at improving group aggregates (though the
benchmark I'll show happens to involve a group aggregate, simply
because the technique works best with large and expensive index
scans).

I see that the patch is not supposed to deal with aggregates in any special
way. But from what I understand after a quick review, skip scan is not getting
applied to them if there are no quals in the query (in that case
_bt_preprocess_keys returns before calling _bt_preprocess_array_keys). Yet such
queries could benefit from skipping, I assume they still could be handled by
the machinery introduced in this patch?

Currently, there is an assumption that "there will be 10 primitive index scans
per skipped attribute". Is any chance to use pg_stats.n_distinct?

It probably makes sense to use pg_stats.n_distinct here. But how?

If the problem is that we're too pessimistic, then I think that this
will usually (though not always) make us more pessimistic. Isn't that
the wrong direction to go in? (We're probably also too optimistic in
some cases, but being too pessimistic is a bigger problem in
practice.)

For example, your test case involved 11 distinct values in each
column. The current approach of hard-coding 10 (which is just a
temporary hack) should actually make the scan look a bit cheaper than
it would if we used the true ndistinct.

Another underlying problem is that the existing SAOP costing really
isn't very accurate, without skip scan -- that's a big source of the
pessimism with arrays/skipping. Why should we be able to get the true
number of primitive index scans just by multiplying together each
omitted prefix column's ndistinct? That approach is good for getting
the worst case, which is probably relevant -- but it's probably not a
very good assumption for the average case. (Though at least we can cap
the total number of primitive index scans to 1/3 of the total number
of pages in the index in btcostestimate, since we have guarantees
about the worst case as of Postgres 17.)

Do I understand correctly, that the only way how multiplying ndistincts could
produce too pessimistic results is when there is a correlation between distinct
values? Can one benefit from the extended statistics here?

And while we're at it, I think it would be great if the implementation will
allow some level of visibility about the skip scan. From what I see, currently
it's by design impossible for users to tell whether something was skipped or
not. But when it comes to planning and estimates, maybe it's not a bad idea to
let explain analyze show something like "expected number of primitive scans /
actual number of primitive scans".

In reply to: Dmitry Dolgov (#14)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Sat, Aug 3, 2024 at 3:34 PM Dmitry Dolgov <9erthalion6@gmail.com> wrote:

I see that the patch is not supposed to deal with aggregates in any special
way.

Right.

But from what I understand after a quick review, skip scan is not getting
applied to them if there are no quals in the query (in that case
_bt_preprocess_keys returns before calling _bt_preprocess_array_keys).

Right.

Yet such queries could benefit from skipping, I assume they still could be handled by
the machinery introduced in this patch?

I'm not sure.

There are no real changes required inside _bt_advance_array_keys with
this patch -- skip arrays are dealt with in essentially the same way
as conventional arrays (as of Postgres 17). I suspect that loose index
scan would be best implemented using _bt_advance_array_keys. It could
also "plug in" to the existing _bt_advance_array_keys design, I
suppose.

As I touched on already, your loose index scan patch applies
high-level semantic information in a way that is very different to my
skip scan patch. This means that it makes revisions to the index AM
API (if memory serves it adds a callback called amskip to that API).
It also means that loose index scan can actually avoid heap accesses;
loose scans wholly avoid accessing logical rows (in both the index and
the heap) by reasoning that it just isn't necessary to do so at all.
Skipping happens in both data structures. Right?

Obviously, my new skip scan patch cannot possibly reduce the number of
heap page accesses required by a given index scan. Precisely the same
logical rows must be accessed as before. There is no two-way
conversation between the index AM and the table AM about which
rows/row groupings have at least one visible tuple. We're just
navigating through the index more efficiently, without changing any
contract outside of nbtree itself.

The "skip scan" name collision is regrettable. But the fact is that
Oracle, MySQL, and now SQLite all call this feature skip scan. That
feels like the right precedent to follow.

Do I understand correctly, that the only way how multiplying ndistincts could
produce too pessimistic results is when there is a correlation between distinct
values?

Yes, that's one problem with the costing. Not the only one, though.

The true number of primitive index scans depends on the cardinality of
the data. For example, a skip scan might be the cheapest plan by far
if (say) 90% of the index has the same leading column value and the
remaining 10% has totally unique values. We'd still do a bad job of
costing this query with an accurate ndistinct for the leading column.
We really one need to do one or two primitive index scans for "the
first 90% of the index", and one more primitive index scan for "the
remaining 10% of the index". For a query such as this, we "require a
full index scan for the remaining 10% of the index", which is
suboptimal, but doesn't fundamentally change anything (I guess that a
skip scan is always suboptimal, in the sense that you could always do
better by having more indexes).

Can one benefit from the extended statistics here?

I really don't know. Certainly seems possible in cases with more than
one skipped leading column.

The main problem with the costing right now is that it's just not very
well thought through, in general. The performance at runtime depends
on the layout of values in the index itself, so the underlying way
that you'd model the costs doesn't have any great precedent in
costsize.c. We do have some idea of the number of leaf pages we'll
access in btcostestimate(), but that works in a way that isn't really
fit for purpose. It kind of works with one primitive index scan, but
works much less well with multiple primitive scans.

And while we're at it, I think it would be great if the implementation will
allow some level of visibility about the skip scan. From what I see, currently
it's by design impossible for users to tell whether something was skipped or
not. But when it comes to planning and estimates, maybe it's not a bad idea to
let explain analyze show something like "expected number of primitive scans /
actual number of primitive scans".

I agree. I think that that's pretty much mandatory for this patch. At
least the actual number of primitive scans should be exposed. Not
quite as sure about showing the estimated number, since that might be
embarrassingly wrong quite regularly, without it necessarily mattering
that much (I'd worry that it'd be distracting).

Displaying the number of primitive scans would already be useful for
index scans with SAOPs, even without this patch. The same general
concepts (estimated vs. actual primitive index scans) already exist,
as of Postgres 17. That's really nothing new.

--
Peter Geoghegan

In reply to: Peter Geoghegan (#15)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Sat, Aug 3, 2024 at 6:14 PM Peter Geoghegan <pg@bowt.ie> wrote:

Displaying the number of primitive scans would already be useful for
index scans with SAOPs, even without this patch. The same general
concepts (estimated vs. actual primitive index scans) already exist,
as of Postgres 17. That's really nothing new.

We actually expose this via instrumentation, in a certain sense. This
is documented by a "Note":

https://www.postgresql.org/docs/devel/monitoring-stats.html#MONITORING-PG-STAT-ALL-INDEXES-VIEW

That is, we already say "Each internal primitive index scan increments
pg_stat_all_indexes.idx_scan, so it's possible for the count of index
scans to significantly exceed the total number of index scan executor
node executions". So, as I said in the last email, advertising the
difference between # of primitive index scans and # of index scan
executor node executions in EXPLAIN ANALYZE is already a good idea.

--
Peter Geoghegan

In reply to: Peter Geoghegan (#13)
3 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Jul 24, 2024 at 5:14 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v4

Attached is v5, which splits the code from v4 patch into 2 pieces --
it becomes 0002-* and 0003-*. Certain refactoring work now appears
under its own separate patch/commit -- see 0002-* (nothing new here,
except the commit message/patch structure). The patch that actually
adds skip scan (0003-* in this new version) has been further polished,
though not in a way that I think is interesting enough to go into
here.

The interesting and notable change for v5 is the addition of the code
in 0001-*. The new 0001-* patch is concerned with certain aspects of
how _bt_advance_array_keys decides whether to start another primitive
index scan (or to stick with the ongoing one for one more leaf page
instead). This is a behavioral change, albeit a subtle one. It's also
kinda independent of skip scan (more on why that is at the end).

It's easiest to explain why 0001-* matters by way of an example. My
example will show significantly more internal/root page accesses than
seen on master, though only when 0002-* and 0003-* are applied, and
0001-* is omitted. When all 3 v5 patches are applied together, the
total number of index pages accessed by the test query will match the
master branch. It's important that skip scan never loses by much to
the master branch, of course. Even when the details of the index/scan
are inconvenient to the implementation, in whatever way.

Setup:

create table demo (int4 a, numeric b);
create index demo_idx on demo (a, b);
insert into demo select a, random() from generate_series(1, 10000) a,
generate_series(1,5) five_rows_per_a_val;
vacuum demo;

We now have a btree index "demo_idx", which has two levels (a root
page plus a leaf level). The root page contains several hundred pivot
tuples, all of which have their "b" value truncated away (or have the
value -inf, if you prefer), with just one prefix "a" column left in
place. Naturally, every leaf page has a high key with its own
separator key that matches one particular tuple that appears in the
root page (except for the rightmost leaf page). So our leaf level scan
will see lots of truncated leaf page high keys (all matching a
corresponding root page tuple).

Test query:

select a from demo where b > 0.99;

This is a query that really shouldn't be doing any skipping at all. We
nevertheless still see a huge amount of skipping with this query, ocne
0001-* is omitted. Prior to 0001-*, a new primitive index scan is
started whenever the scan reaches a "boundary" between adjoining leaf
pages. That is, whenever _bt_advance_array_keys stopped on a high key
pstate.finaltup. So without the new 0001-* work, the number of page
accesses almost doubles (because we access the root page once per leaf
page accessed, instead of just accessing it once for the whole scan).

What skip scan should have been doing all along (and will do now) is
to step forward to the next right sibling leaf page whenever it
reaches a boundary between leaf pages. This should happen again and
again, without our ever choosing to start a new primitive index scan
instead (it shouldn't happen even once with this query). In other
words, we ought to behave just like a full index scan would behave
with this query -- which is exactly what we get on master.

The scan will still nominally "use skip scan" even with this fix in
place, but in practice, for this particular query/index, the scan
won't ever actually decide to skip. So it at least "looks like" an
index scan from the point of view of EXPLAIN (ANALYZE, BUFFERS). There
is a separate question of how many CPU cycles we use to do all this,
but for now my focus is on total pages accessed by the patch versus on
master, especially for adversarial cases such as this.

It should be noted that the skip scan patch never had any problems
with this very similar query (same table as before):

select a from demo where b < 0.01;

The fact that we did the wrong thing for the first query, but the
right thing for this second similar query, was solely due to certain
accidental implementation details -- it had nothing to do with the
fundamentals of the problem. You might even say that 0001-* makes the
original "b > 0.99" case behave in the same manner as this similar "b
< 0.01" case, which is justifiable on consistency grounds. Why
wouldn't these two cases behave similarly? It's only logical.

The underlying problem arguably has little to do with skip scan;
whether we use a real SAOP array on "a" or a consed up skip array is
incidental to the problem that my example highlights. As always, the
underlying "array type" (skip vs SOAP) only matters to the lowest
level code. And so technically, this is an existing issue on
HEAD/master. You can see that for yourself by making the problematic
query's qual "where a = any ('every possible a value') and b > 0.99"
-- same problem on Postgres 17, without involving skip scan.

To be sure, the underlying problem does become more practically
relevant with the invention of skip arrays for skip scan, but 0001-*
can still be treated as independent work. It can be committed well
ahead of the other stuff IMV. The same is likely also true of the
refactoring now done in 0002-* -- it does refactoring that makes
sense, even without skip scan. And so I don't expect it to take all
that long for it to be committable.

--
Peter Geoghegan

Attachments:

v5-0001-Normalize-nbtree-truncated-high-key-array-behavio.patchapplication/octet-stream; name=v5-0001-Normalize-nbtree-truncated-high-key-array-behavio.patchDownload
From 8c8a3c36daa9c3f69aab6024d0d44e71451fae3b Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Thu, 8 Aug 2024 13:51:18 -0400
Subject: [PATCH v5 1/3] Normalize nbtree truncated high key array behavior.

Commit 5bf748b8 taught nbtree ScalarArrayOp array processing to decide
when and how to start the next primitive index scan based on physical
index characteristics.  This included rules for deciding whether to
start a new primitive index scan (or whether to move onto the right
sibling leaf page instead) whenever the scan encounters a leaf high key
with truncated lower-order columns whose omitted/-inf values are covered
by one or more arrays.

Prior to this commit, nbtree would treat a truncated column as
satisfying a scan key that marked required in the current scan
direction.  It would just give up and start a new primitive index scan
in cases involving inequalities required in the opposite direction only
(in practice this meant > and >= strategy scan keys, since only forward
scans consider the page high key like this).

Bring > and >= strategy scan keys in line with other required scan key
types: have nbtree persist with its current primitive index scan
regardless of the operator strategy in use.  This requires scheduling
and then performing an explicit check of the next page's high key (if
any) at the point that _bt_readpage is next called.

Although this could be considered a stand alone piece of work, it's
mostly intended as preparation for an upcoming patch that adds skip scan
optimizations to nbtree.  Without this work there are cases where the
scan's skip arrays trigger an excessive number of primitive index scans
due to most high keys having a truncated attribute that was previously
treated as not satisfying a required > or >= strategy scan key.
---
 src/include/access/nbtree.h           |   3 +
 src/backend/access/nbtree/nbtree.c    |   4 +
 src/backend/access/nbtree/nbtsearch.c |  24 ++++++
 src/backend/access/nbtree/nbtutils.c  | 119 ++++++++++++++------------
 4 files changed, 97 insertions(+), 53 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 749304334..5f366323c 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1048,6 +1048,7 @@ typedef struct BTScanOpaqueData
 	int			numArrayKeys;	/* number of equality-type array keys */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Last array advancement matched -inf attr? */
+	bool		oppoDirCheck;	/* check opposite dir scan keys? */
 	BTArrayKeyInfo *arrayKeys;	/* info about each equality-type array key */
 	FmgrInfo   *orderProcs;		/* ORDER procs for required equality keys */
 	MemoryContext arrayContext; /* scan-lifespan context for array data */
@@ -1291,6 +1292,8 @@ extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir);
 extern void _bt_preprocess_keys(IndexScanDesc scan);
 extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 						  IndexTuple tuple, int tupnatts);
+extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+								  IndexTuple finaltup);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 686a3206f..e5ce129cc 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -331,6 +331,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 
 	so->needPrimScan = false;
 	so->scanBehind = false;
+	so->oppoDirCheck = false;
 	so->arrayKeys = NULL;
 	so->orderProcs = NULL;
 	so->arrayContext = NULL;
@@ -374,6 +375,7 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 	so->markItemIndex = -1;
 	so->needPrimScan = false;
 	so->scanBehind = false;
+	so->oppoDirCheck = false;
 	BTScanPosUnpinIfPinned(so->markPos);
 	BTScanPosInvalidate(so->markPos);
 
@@ -626,6 +628,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 		 */
 		so->needPrimScan = false;
 		so->scanBehind = false;
+		so->oppoDirCheck = false;
 	}
 	else
 	{
@@ -670,6 +673,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 				}
 				so->needPrimScan = true;
 				so->scanBehind = false;
+				so->oppoDirCheck = false;
 				*pageno = InvalidBlockNumber;
 				exit_loop = true;
 			}
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 57bcfc7e4..88f4ef7b7 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1679,6 +1679,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 		ItemId		iid;
 		IndexTuple	itup;
 
+		Assert(!so->oppoDirCheck);
+
 		iid = PageGetItemId(page, ScanDirectionIsForward(dir) ? maxoff : minoff);
 		itup = (IndexTuple) PageGetItem(page, iid);
 
@@ -1696,6 +1698,28 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			ItemId		iid = PageGetItemId(page, P_HIKEY);
 
 			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+			if (unlikely(so->oppoDirCheck))
+			{
+				/*
+				 * Last _bt_readpage call scheduled precheck of finaltup for
+				 * required scan keys up to and including a > or >= scan key
+				 * (necessary because > and >= are only generally considered
+				 * required when scanning backwards)
+				 */
+				Assert(so->scanBehind);
+				so->oppoDirCheck = false;
+				if (!_bt_oppodir_checkkeys(scan, dir, pstate.finaltup))
+				{
+					/*
+					 * Back out of continuing with this leaf page -- schedule
+					 * another primitive index scan after all
+					 */
+					so->currPos.moreRight = false;
+					so->needPrimScan = true;
+					return false;
+				}
+			}
 		}
 
 		/* load items[] in ascending order */
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index d6de2072d..1b39d8701 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -1362,7 +1362,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 			curArrayKey->cur_elem = 0;
 		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
 	}
-	so->scanBehind = false;
+	so->scanBehind = so->oppoDirCheck = false;	/* reset */
 }
 
 /*
@@ -1671,8 +1671,7 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
 
 	Assert(so->numArrayKeys);
 
-	/* scanBehind flag doesn't persist across primitive index scans - reset */
-	so->scanBehind = false;
+	so->scanBehind = so->oppoDirCheck = false;	/* reset */
 
 	/*
 	 * Array keys are advanced within _bt_checkkeys when the scan reaches the
@@ -1808,7 +1807,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 
-		so->scanBehind = false; /* reset */
+		so->scanBehind = so->oppoDirCheck = false;	/* reset */
 
 		/*
 		 * Required scan key wasn't satisfied, so required arrays will have to
@@ -2293,19 +2292,27 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	if (so->scanBehind && has_required_opposite_direction_only)
 	{
 		/*
-		 * However, we avoid this behavior whenever the scan involves a scan
+		 * However, we do things differently whenever the scan involves a scan
 		 * key required in the opposite direction to the scan only, along with
 		 * a finaltup with at least one truncated attribute that's associated
 		 * with a scan key marked required (required in either direction).
 		 *
 		 * _bt_check_compare simply won't stop the scan for a scan key that's
 		 * marked required in the opposite scan direction only.  That leaves
-		 * us without any reliable way of reconsidering any opposite-direction
+		 * us without an automatic way of reconsidering any opposite-direction
 		 * inequalities if it turns out that starting a new primitive index
 		 * scan will allow _bt_first to skip ahead by a great many leaf pages
 		 * (see next section for details of how that works).
+		 *
+		 * We deal with this by explicitly scheduling a finaltup recheck for
+		 * the next page -- we'll call _bt_oppodir_checkkeys for the next
+		 * page's finaltup instead.  You can think of this as a way of dealing
+		 * with this page's finaltup being truncated by checking the next
+		 * page's finaltup instead.  And you can think of the oppoDirCheck
+		 * recheck handling within _bt_readpage as complementing the similar
+		 * scanBehind recheck made from within _bt_checkkeys.
 		 */
-		goto new_prim_scan;
+		so->oppoDirCheck = true;	/* schedule next page's finaltup recheck */
 	}
 
 	/*
@@ -2343,54 +2350,16 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * also before the _bt_first-wise start of tuples for our new qual.  That
 	 * at least suggests many more skippable pages beyond the current page.
 	 */
-	if (has_required_opposite_direction_only && pstate->finaltup &&
-		(all_required_satisfied || oppodir_inequality_sktrig))
+	else if (has_required_opposite_direction_only && pstate->finaltup &&
+			 (all_required_satisfied || oppodir_inequality_sktrig) &&
+			 unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup)))
 	{
-		int			nfinaltupatts = BTreeTupleGetNAtts(pstate->finaltup, rel);
-		ScanDirection flipped;
-		bool		continuescanflip;
-		int			opsktrig;
-
 		/*
-		 * We're checking finaltup (which is usually not caller's tuple), so
-		 * cannot reuse work from caller's earlier _bt_check_compare call.
-		 *
-		 * Flip the scan direction when calling _bt_check_compare this time,
-		 * so that it will set continuescanflip=false when it encounters an
-		 * inequality required in the opposite scan direction.
+		 * Make sure that any non-required arrays are set to the first array
+		 * element for the current scan direction
 		 */
-		Assert(!so->scanBehind);
-		opsktrig = 0;
-		flipped = -dir;
-		_bt_check_compare(scan, flipped,
-						  pstate->finaltup, nfinaltupatts, tupdesc,
-						  false, false, false,
-						  &continuescanflip, &opsktrig);
-
-		/*
-		 * Only start a new primitive index scan when finaltup has a required
-		 * unsatisfied inequality (unsatisfied in the opposite direction)
-		 */
-		Assert(all_required_satisfied != oppodir_inequality_sktrig);
-		if (unlikely(!continuescanflip &&
-					 so->keyData[opsktrig].sk_strategy != BTEqualStrategyNumber))
-		{
-			/*
-			 * It's possible for the same inequality to be unsatisfied by both
-			 * caller's tuple (in scan's direction) and finaltup (in the
-			 * opposite direction) due to _bt_check_compare's behavior with
-			 * NULLs
-			 */
-			Assert(opsktrig >= sktrig); /* not opsktrig > sktrig due to NULLs */
-
-			/*
-			 * Make sure that any non-required arrays are set to the first
-			 * array element for the current scan direction
-			 */
-			_bt_rewind_nonrequired_arrays(scan, dir);
-
-			goto new_prim_scan;
-		}
+		_bt_rewind_nonrequired_arrays(scan, dir);
+		goto new_prim_scan;
 	}
 
 	/*
@@ -3522,7 +3491,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 *
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
-		Assert(!so->scanBehind && !pstate->prechecked && !pstate->firstmatch);
+		Assert(!so->scanBehind && !so->oppoDirCheck);
+		Assert(!pstate->prechecked && !pstate->firstmatch);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
@@ -3634,6 +3604,49 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 								  ikey, true);
 }
 
+/*
+ * Test whether an indextuple satisfies inequalities required in the opposite
+ * direction only (and lower-order equalities required in either direction).
+ *
+ * scan: index scan descriptor (containing a search-type scankey)
+ * dir: current scan direction (flipped by us to get opposite direction)
+ * finaltup: final index tuple on the page
+ *
+ * Caller's finaltup tuple is the page high key (for forwards scans), or the
+ * first non-pivot tuple (for backwards scans).  Caller during scans with
+ * required array keys.
+ *
+ * Return true if finatup satisfies keys, false if not.  If the tuple fails to
+ * pass the qual, then caller is should start another primitive index scan;
+ * _bt_first can efficiently relocate the scan to a far later leaf page.
+ *
+ * Note: we focus on required-in-opposite-direction scan keys (e.g. for a
+ * required > or >= key, assuming a forwards scan) because _bt_checkkeys() can
+ * always deal with required-in-current-direction scan keys on its own.
+ */
+bool
+_bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+					  IndexTuple finaltup)
+{
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	int			nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+	bool		continuescan;
+	ScanDirection flipped = -dir;
+	int			ikey = 0;
+
+	Assert(so->numArrayKeys);
+
+	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
+					  false, false, false, &continuescan, &ikey);
+
+	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
+		return false;
+
+	return true;
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
-- 
2.45.2

v5-0003-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v5-0003-Add-skip-scan-to-nbtree.patchDownload
From e4b75773628cd492edd12addeda8cbbac618b749 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v5 3/3] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on an index (a, b) for queries with a predicate such as "WHERE b = 5".
This is useful in cases where the total number of distinct values in the
column 'a' is reasonably small (think hundreds, possibly thousands).

In effect, a skip scan treats the composite index on (a, b) as if it was
a series of disjunct subindexes -- one subindex per distinct 'a' value.
We exhaustively "search every subindex" using a qual that behaves like
"WHERE a = ANY(<every possible 'a' value>) AND b = 5".

The design of skip scan works by extended the design for arrays
established by commit 5bf748b8.  "Skip arrays" generate their array
values procedurally and on-demand, but otherwise work just like arrays
used by SAOPs.

B-Tree operator classes on discrete types can now optionally provide a
skip support routine.  This is used to generate the next array element
value by incrementing the current value (or by decrementing, in the case
of backwards scans).  When the opclass lacks a skip support routine, we
use sentinel next-key values instead.  Adding skip support makes skip
scans more efficient in cases where there is naturally a good chance
that the very next value will find matching tuples.  For example, during
an index scan with a leading "sales_date" attribute, there is a decent
chance that a scan that just finished returning tuples matching
"sales_date = '2024-06-01' and id = 5000" will find later tuples
matching "sales_date = '2024-06-02' and id = 5000".  It is to our
advantage to skip straight to the relevant "id = 5000" leaf page,
totally avoiding reading earlier "sales_date = '2024-06-02'" leaf pages.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/nbtree.h                 |   25 +-
 src/include/catalog/pg_amproc.dat           |   16 +
 src/include/catalog/pg_proc.dat             |   24 +
 src/include/utils/skipsupport.h             |  109 ++
 src/backend/access/nbtree/nbtcompare.c      |  261 ++++
 src/backend/access/nbtree/nbtsearch.c       |   80 +-
 src/backend/access/nbtree/nbtutils.c        | 1456 +++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c     |    4 +
 src/backend/commands/opclasscmds.c          |   25 +
 src/backend/utils/adt/Makefile              |    1 +
 src/backend/utils/adt/date.c                |   44 +
 src/backend/utils/adt/meson.build           |    1 +
 src/backend/utils/adt/selfuncs.c            |   30 +-
 src/backend/utils/adt/skipsupport.c         |   60 +
 src/backend/utils/adt/uuid.c                |   67 +
 src/backend/utils/misc/guc_tables.c         |   23 +
 doc/src/sgml/btree.sgml                     |   13 +
 doc/src/sgml/indices.sgml                   |   40 +-
 doc/src/sgml/xindex.sgml                    |   16 +-
 src/test/regress/expected/alter_generic.out |    6 +-
 src/test/regress/expected/psql.out          |    3 +-
 src/test/regress/sql/alter_generic.sql      |    2 +-
 src/tools/pgindent/typedefs.list            |    3 +
 23 files changed, 2175 insertions(+), 134 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 5f366323c..13a650c3d 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1031,10 +1033,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard arrays that store elements in memory */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup set to valid routine? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* opclass skip scan support, when use_sksup */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo	order_low;		/* low_compare's ORDER proc */
+	FmgrInfo	order_high;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1124,6 +1138,9 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array, for skip scan */
+#define SK_BT_NEGPOSINF	0x00080000	/* no sk_argument, -inf/+inf key */
+#define SK_BT_NEXTPRIOR	0x00100000	/* sk_argument is next/prior key */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1160,6 +1177,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index f639c3a6a..2a8f6f3f1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -122,6 +128,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +149,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +170,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +205,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +275,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d36f6001b..2dec83363 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2214,6 +2229,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4401,6 +4419,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -9231,6 +9252,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..d91390fc6
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,109 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * This happens at the point where the scan determines that another primitive
+ * index scan is required.  The next value is used (in combination with at
+ * least one additional lower-order non-skip key, taken from the SQL query) to
+ * relocate the scan, skipping over many irrelevant leaf pages in the process.
+ *
+ * Skip support generally works best with discrete types such as integer,
+ * date, and boolean; types where there is a decent chance that indexes will
+ * contain contiguous values (given a leading attributes using the opclass).
+ * When gaps/discontinuities are naturally rare (e.g., a leading identity
+ * column in a composite index, a date column preceding a product_id column),
+ * then it makes sense for skip scans to optimistically assume that the next
+ * distinct indexable value will find directly matching index tuples.
+ *
+ * The B-Tree code can fall back on next-key sentinel values for any opclass
+ * that doesn't provide its own skip support function.  There is no point in
+ * providing skip support unless the next indexed key value is often the next
+ * indexable value (at least with some workloads).  Opclasses where that never
+ * works out in practice should just rely on the B-Tree AM's generic next-key
+ * fallback strategy.  Opclasses where adding skip support is infeasible or
+ * hard (e.g., an opclass for a continuous type) can also use the fallback.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called.
+ * If an opclass can only set some of the fields, then it cannot safely
+ * provide a skip support routine.
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming standard ascending
+	 * order).  This helps the B-Tree code with finding its initial position
+	 * at the leaf level (during the skip scan's first primitive index scan).
+	 * In other words, it gives the B-Tree code a useful value to start from,
+	 * before any data has been read from the index.
+	 *
+	 * low_elem and high_elem are also used by skip scans to determine when
+	 * they've reached the final possible value (in the current direction).
+	 * It's typical for the scan to run out of leaf pages before it runs out
+	 * of unscanned indexable values, but it's still useful for the scan to
+	 * have a way to recognize when it has reached the last possible value
+	 * (this saves us a useless probe that just lands on the final leaf page).
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (in the case of pass-by-reference
+	 * types).  It's not okay for these functions to leak any memory.
+	 *
+	 * Both decrement and increment callbacks are guaranteed to never be
+	 * called with a NULL "existing" arg.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is undefined, and the B-Tree
+	 * code is entitled to assume that no memory will have been allocated.
+	 *
+	 * The B-Tree skip scan caller's "existing" datum is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must be liberal
+	 * in accepting every possible representational variation within the
+	 * underlying data type.  On the other hand, opclasses are _not_ expected
+	 * to preserve any information that doesn't affect how datums are sorted
+	 * (e.g., skip support for a fixed precision numeric type isn't required
+	 * to preserve datum display scale).
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..deb387453 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,49 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +149,49 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +215,49 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +301,49 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +465,49 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +541,48 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 88f4ef7b7..05b98efc2 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -880,7 +880,6 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	Buffer		buf;
 	BTStack		stack;
 	OffsetNumber offnum;
-	StrategyNumber strat;
 	BTScanInsertData inskey;
 	ScanKey		startKeys[INDEX_MAX_KEYS];
 	ScanKeyData notnullkeys[INDEX_MAX_KEYS];
@@ -1022,6 +1021,8 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		ScanKey		chosen;
 		ScanKey		impliesNN;
 		ScanKey		cur;
+		int			ikey = 0,
+					ichosen = 0;
 
 		/*
 		 * chosen is the so-far-chosen key for the current attribute, if any.
@@ -1042,6 +1043,53 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		{
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
+				/*
+				 * Conceptually, skip arrays consist of array elements whose
+				 * values are generated procedurally and on demand.  We need
+				 * special handling for that here.
+				 *
+				 * We must interpret various sentinel values to generate an
+				 * insertion scan key.  This is only actually needed for index
+				 * attributes whose input opclass lacks a skip support routine
+				 * (when skip support is available we'll always be able to
+				 * generate true array element datum values instead).
+				 */
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					ScanKey		origchosen = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (; ikey < so->numArrayKeys; ikey++)
+					{
+						array = &so->arrayKeys[ikey];
+						if (array->scan_key == ichosen)
+							break;
+					}
+
+					/* use array's inequality key in startKeys[] */
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					if (!chosen && !array->null_elem)
+					{
+						/*
+						 * Array doesn't have any explicit low_compare or
+						 * high_compare that we can use (given the current
+						 * scan direction).  The array does not include a NULL
+						 * element (to generate an IS NULL qual), though, so
+						 * we might need to deduce a NOT NULL key to skip over
+						 * any NULLs.  Prepare for that.
+						 *
+						 * Note: this is also how we handle an explicit NOT
+						 * NULL key that preprocessing folded into the skip
+						 * array.
+						 */
+						impliesNN = origchosen;
+					}
+				}
+
 				/*
 				 * Done looking at keys for curattr.  If we didn't find a
 				 * usable boundary key, see if we can deduce a NOT NULL key.
@@ -1075,16 +1123,34 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 				startKeys[keysz++] = chosen;
 
+				/*
+				 * Skip arrays can also use a sk_argument which is marked
+				 * "next key".  This is another sentinel array element value
+				 * requiring special handling here by us.  As with -inf/+inf
+				 * sentinels, there cannot be any exact non-pivot matches.
+				 */
+				if (chosen->sk_flags & SK_BT_NEXTPRIOR)
+				{
+					/*
+					 * Adjust strat_total, so that our = key gets treated like
+					 * a > key (or like a < key)
+					 */
+					if (ScanDirectionIsForward(dir))
+						strat_total = BTGreaterStrategyNumber;
+					else
+						strat_total = BTLessStrategyNumber;
+					break;
+				}
+
 				/*
 				 * Adjust strat_total, and quit if we have stored a > or <
 				 * key.
 				 */
-				strat = chosen->sk_strategy;
-				if (strat != BTEqualStrategyNumber)
+				if (chosen->sk_strategy != BTEqualStrategyNumber)
 				{
-					strat_total = strat;
-					if (strat == BTGreaterStrategyNumber ||
-						strat == BTLessStrategyNumber)
+					strat_total = chosen->sk_strategy;
+					if (chosen->sk_strategy == BTGreaterStrategyNumber ||
+						chosen->sk_strategy == BTLessStrategyNumber)
 						break;
 				}
 
@@ -1103,6 +1169,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 				curattr = cur->sk_attno;
 				chosen = NULL;
 				impliesNN = NULL;
+				ichosen = -1;
 			}
 
 			/*
@@ -1127,6 +1194,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 				case BTEqualStrategyNumber:
 					/* override any non-equality choice */
 					chosen = cur;
+					ichosen = i;
 					break;
 				case BTGreaterEqualStrategyNumber:
 				case BTGreaterStrategyNumber:
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 7fa977a62..fa046c550 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND c > 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c' (and so 'c' will still have an inequality scan key,
+ * required in only one direction -- 'c' won't be output as a "range" skip
+ * key/array).
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using opclass skip
+ * support routines.  This can be used to quantify the peformance benefit that
+ * comes from having dedicated skip support, with a given test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup set to valid routine? */
+	Oid			eq_op;			/* InvalidOid means don't skip */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -64,15 +92,38 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno,
+							BTSkipPreproc *skipatts);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
-										   Datum arrdatum, ScanKey cur);
+										   Datum arrdatum, bool arrnull,
+										   ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -258,11 +309,19 @@ _bt_freestack(BTStack stack)
  * preprocessing steps are complete.  This will convert the scan key offset
  * references into references to the scan's so->keyData[] output scan keys.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by later preprocessing on output.
+ * _bt_decide_skipatts decides which attributes receive skip arrays.
+ *
  * Caller must pass *numberOfKeys to give us a way to change the number of
  * input scan keys (our output is caller's input).  The returned array can be
  * smaller than scan->keyData[] when we eliminated a redundant array scan key
- * (redundant with some other array scan key, for the same attribute).  Caller
- * uses this to allocate so->keyData[] for the current btrescan.
+ * (redundant with some other array scan key, for the same attribute).  It can
+ * also be larger when we added a skip array/skip scan key.  Caller uses this
+ * to allocate so->keyData[] for the current btrescan.
  *
  * Note: the reason we need to return a temp scan key array, rather than just
  * scribbling on scan->keyData, is that callers are permitted to call btrescan
@@ -275,8 +334,11 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 	Relation	rel = scan->indexRelation;
 	int			numArrayKeyData = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
+				numSkipArrayKeys,
 				output_ikey = 0;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -286,7 +348,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 
 	Assert(scan->numberOfKeys);
 
-	/* Quick check to see if there are any array keys */
+	/*
+	 * Quick check to see if there are any array keys, or any missing keys we
+	 * can generate a "skip scan" array key for ourselves
+	 */
 	numArrayKeys = 0;
 	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
@@ -304,6 +369,16 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 		}
 	}
 
+	/* Consider generating skip arrays, and associated equality scan keys */
+	numSkipArrayKeys = _bt_decide_skipatts(scan, skipatts);
+	if (numSkipArrayKeys)
+	{
+		/* At least one skip array scan key must be added to arrayKeyData[] */
+		numArrayKeys += numSkipArrayKeys;
+		/* output scan key buffer allocation needs space for skip scan keys */
+		numArrayKeyData += numSkipArrayKeys;
+	}
+
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
@@ -330,7 +405,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
 	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/*
+	 * Process each array key, and generate skip arrays as needed.  Also copy
+	 * every scan->keyData[] input scan key (whether it's an array or not)
+	 * into the arrayKeyData array we'll return to our caller (barring any
+	 * array scan keys that we could eliminate early through array merging).
+	 */
 	numArrayKeys = 0;
 	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
@@ -348,6 +428,73 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and scan key where indicated by skipatts */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* won't skip using this attribute */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[output_ikey];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * Temporary testing GUC can disable the use of an opclass's skip
+			 * support routine
+			 */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how the
+			 * consed-up "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[output_ikey], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			output_ikey++;		/* keep this scan key/array */
+		}
+
 		/*
 		 * Copy input scan key into temp arrayKeyData scan key array.  (From
 		 * here on, cur points at our copy of the input scan key.)
@@ -522,6 +669,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
 		numArrayKeys++;
 		output_ikey++;			/* keep this scan key/array */
 	}
@@ -635,7 +786,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -696,6 +848,211 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_decide_skipatts() -- set index attributes requiring skip arrays
+ *
+ * _bt_preprocess_array_keys helper function.  Determines which attributes
+ * will require skip arrays/scan keys.  Also sets up skip support callbacks
+ * for attributes whose input opclass have skip support (opclasses without
+ * skip support will fall back on using next-key sentinel values when
+ * advancing the skip array to its next array element).
+ *
+ * Return value is the total number of scan keys to add as "input" scan keys
+ * for further processing within _bt_preprocess_keys.
+ */
+static int
+_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts)
+{
+	Relation	rel = scan->indexRelation;
+	ScanKey		inputsk;
+	AttrNumber	attno_inputsk = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numSkipArrayKeys = 0,
+				prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * FIXME Don't support parallel index scans for now.
+	 *
+	 * _bt_parallel_primscan_schedule must be taught to account for skip
+	 * arrays. This is likely to require that we store the current array
+	 * element datum in shared memory.
+	 */
+	if (scan->parallel_scan)
+		return 0;
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan.
+	 */
+	inputsk = &scan->keyData[0];
+	for (int i = 0;; inputsk++, i++)
+	{
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inputsk
+		 */
+		while (attno_skip < attno_inputsk)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Opclass lacks a suitable skip support routine.
+				 *
+				 * Return prev_numSkipArrayKeys, so as to avoid including any
+				 * "backfilled" arrays that were supposed to form a contiguous
+				 * group with a skip array on this attribute.  There is no
+				 * benefit to adding backfill skip arrays unless we can do so
+				 * for all attributes (all attributes up to and including the
+				 * one immediately before attno_inputsk).
+				 */
+				return prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			numSkipArrayKeys++;
+			attno_skip++;
+		}
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i >= scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 * (either in part or in whole)
+		 */
+		if (attno_inputsk > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inputsk (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inputsk < inputsk->sk_attno)
+		{
+			prev_numSkipArrayKeys = numSkipArrayKeys;
+
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (!attno_has_equal)
+			{
+				/* Only saw inequalities for the prior attribute */
+				if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+				{
+					/* add a range skip array for this attribute */
+					numSkipArrayKeys++;
+				}
+				else
+					break;
+			}
+			else
+			{
+				/*
+				 * Saw an equality for the prior attribute, so it doesn't need
+				 * a skip array (not even a range skip array)
+				 */
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inputsk = inputsk->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this scan key's attribute has any equality strategy scan
+		 * keys.
+		 *
+		 * Treat IS NULL scan keys as using equal strategy (they'll be marked
+		 * as using it later on, by _bt_fix_scankey_strategy).
+		 */
+		if (inputsk->sk_strategy == BTEqualStrategyNumber ||
+			(inputsk->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.
+		 *
+		 * We do still backfill skip attributes before the RowCompare, so that
+		 * it can be marked required.  This is similar to what happens when a
+		 * conventional inequality uses an opclass that lacks skip support.
+		 */
+		if (inputsk->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	return numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatts
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false.
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatts)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatts->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										  BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect input opclasses lacking even an equality
+	 * operator, but they're still supported.  Deal with them gracefully.
+	 */
+	if (!OidIsValid(skipatts->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatts->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+														reverse,
+														&skipatts->sksup);
+
+	/* might not have set up skip support routine, but can skip either way */
+	return true;
+}
+
 /*
  * _bt_setup_array_cmp() -- Set up array comparison functions
  *
@@ -988,17 +1345,15 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
@@ -1011,8 +1366,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.  We have to be sure
+	 * that _bt_compare_array_skey/_bt_binsrch_array_skey use the right proc.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1043,11 +1398,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensured that the scalar scan key could be eliminated as redundant
+		 */
+		eliminated = true;
+	}
+	else
+	{
+		/*
+		 * With a skip array it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when earlier preprocessing wasn't able to eliminate a
+		 * redundant scan key inequality due to a lack of cross-type support.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when it
+ * is redundant with (or contradicted by) a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1099,6 +1508,137 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of skip array scan key when it is "redundant with"
+ * a non-array scalar scan key.  The scalar scan key must be an inequality.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array, allowing its elements to be generated within the limits of a range.
+ * Calling here always renders caller's scalar scan key redundant (the key is
+ * applied when the array advances, but that's just an implementation detail).
+ *
+ * Return value indicates if the array already had a lower/upper bound
+ * (whichever caller's scalar scan key was expected to be).  We return true in
+ * the common case where caller's scan key could be successfully rolled into
+ * the skip array.  We return false when we can't do that due to the presence
+ * of a conflicting inequality.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	/*
+	 * We don't expect to have to deal with NULLs in non-array/non-skip scan
+	 * key.  We expect _bt_preprocess_array_keys to avoid generating a skip
+	 * array for an index attribute with an IS NULL input scan key.  (It will
+	 * still do so in the presence of IS NOT NULL input scan keys, but
+	 * _bt_compare_scankey_args is expected to handle those for us.)
+	 */
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(arraysk->sk_flags & SK_SEARCHARRAY);
+	Assert(arraysk->sk_strategy == BTEqualStrategyNumber);
+	Assert(array->num_elems == -1);
+
+	/* Scalar scan key must be a B-Tree inequality, which are always strict */
+	Assert(!(skey->sk_flags & SK_ISNULL));
+	Assert(skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * A skip array scan key always uses the underlying index attribute's
+	 * input opclass, but it's possible that caller's scalar scan key uses a
+	 * cross-type operator.  In cross-type scenarios, skey.sk_argument doesn't
+	 * use the same type as later array elements (which are all just copies of
+	 * datums taken from index tuples, possibly modified by skip support).
+	 *
+	 * We represent the lowest (and highest) possible value in the array using
+	 * the sentinel value -inf (+inf for high_compare).  The only exceptions
+	 * apply when the opclass has skip support: there we can use a copy of the
+	 * skip support routine's low_elem/high_elem instead -- though only when
+	 * there is no corresponding low_compare/high_compare inequality.
+	 *
+	 * _bt_first understands that -inf/+inf indicate that it should use the
+	 * low_compare/high_compare inequality for initial positioning purposes
+	 * when it sees either value (unless there is no corresponding inequality,
+	 * in which case the values are literally interpreted as -inf or +inf).
+	 * _bt_first can therefore vary in whether it uses a cross-type operator,
+	 * or an input-opclass-only operator (it can vary across primitive scans
+	 * for the same index attribute/skip array).
+	 *
+	 * _bt_scankey_decrement/_bt_scankey_increment both make sure that each
+	 * newly generated element is constrained by low_compare/high_compare.
+	 * This must happen without skey.sk_argument ever being treated as a true
+	 * array element (that wouldn't always work because array elements are
+	 * only ever supposed to use the opclass input type).
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare */
+
+				/* replace old high_compare with new one */
+			}
+			else
+				array->high_compare = palloc(sizeof(ScanKeyData));
+
+			memcpy(array->high_compare, skey, sizeof(ScanKeyData));
+			array->order_high = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare */
+
+				/* replace old low_compare with new one */
+			}
+			else
+				array->low_compare = palloc(sizeof(ScanKeyData));
+
+			memcpy(array->low_compare, skey, sizeof(ScanKeyData));
+			array->order_low = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
@@ -1141,7 +1681,8 @@ _bt_compare_array_elements(const void *a, const void *b, void *arg)
 static inline int32
 _bt_compare_array_skey(FmgrInfo *orderproc,
 					   Datum tupdatum, bool tupnull,
-					   Datum arrdatum, ScanKey cur)
+					   Datum arrdatum, bool arrnull,
+					   ScanKey cur)
 {
 	int32		result = 0;
 
@@ -1149,14 +1690,14 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 
 	if (tupnull)				/* NULL tupdatum */
 	{
-		if (cur->sk_flags & SK_ISNULL)
+		if (arrnull)
 			result = 0;			/* NULL "=" NULL */
 		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = -1;		/* NULL "<" NOT_NULL */
 		else
 			result = 1;			/* NULL ">" NOT_NULL */
 	}
-	else if (cur->sk_flags & SK_ISNULL) /* NOT_NULL tupdatum, NULL arrdatum */
+	else if (arrnull)			/* NOT_NULL tupdatum, NULL arrdatum */
 	{
 		if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = 1;			/* NOT_NULL ">" NULL */
@@ -1222,6 +1763,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1257,7 +1800,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[low_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result <= 0)
 				{
@@ -1285,7 +1828,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[high_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result >= 0)
 				{
@@ -1312,7 +1855,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 		arrdatum = array->elem_values[mid_elem];
 
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										arrdatum, cur);
+										arrdatum, false, cur);
 
 		if (result == 0)
 		{
@@ -1337,13 +1880,102 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	 */
 	if (low_elem != mid_elem)
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										array->elem_values[low_elem], cur);
+										array->elem_values[low_elem], false,
+										cur);
 
 	*set_elem_result = result;
 
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * This routine doesn't return an index into the array, because the array
+ * doesn't actually have any elements (it generates its array elements
+ * procedurally instead).  Note that this may include a NULL value/an IS NULL
+ * qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1353,29 +1985,506 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
 		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
 		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, curArrayKey,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppoDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Clear possibly-irrelevant flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Lowest (or highest) element is NULL, so set scan key to NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Lowest array element isn't NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Highest array element isn't NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR);
+
+	/*
+	 * Treat tupdatum/tupnull as a matching array element.
+	 *
+	 * We just copy tupdatum into the array's scan key (there is no
+	 * conventional array element for us to set, of course).
+	 *
+	 * Unlike standard arrays, skip arrays sometimes need to locate NULLs.
+	 * Treat them as just another value from the domain of indexed values.
+	 */
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		underflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & SK_BT_NEXTPRIOR));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the NEXTPRIOR flag.  The true prior value can only
+	 * be determined when the scan reads lower sorting tuples.
+	 *
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, _bt_first can find the highest non-NULL.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * low_compare is for an >= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true prior value
+		 * cannot possibly satisfy low_compare.  We can give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(&array->order_low,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, false,
+								   skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true prior value */
+		skey->sk_flags |= SK_BT_NEXTPRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &underflow);
+
+	if (underflow)
+	{
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/*
+			 * Existing sk_argument was already equal to non-NULL low_elem
+			 * provided by opclass skip support routine, but skip array's true
+			 * lowest element is actually NULL.
+			 *
+			 * Decrement sk_argument to NULL.
+			 */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the skip array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		overflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & SK_BT_NEXTPRIOR));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXTPRIOR flag.  The true next value can only be
+	 * determined when the scan reads higher sorting tuples.
+	 *
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, _bt_first can find the lowest non-NULL.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * high_compare is for an <= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true next value cannot
+		 * possibly satisfy high_compare.  We can give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(&array->order_high,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, false,
+								   skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true next value */
+		skey->sk_flags |= SK_BT_NEXTPRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &overflow);
+
+	if (overflow)
+	{
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/*
+			 * Existing sk_argument was already equal to non-NULL high_elem
+			 * provided by opclass skip support routine, but skip array's true
+			 * highest element is actually NULL.
+			 *
+			 * Increment sk_argument to NULL.
+			 */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the skip array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1391,6 +2500,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1400,29 +2510,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then advance next most significant array, if any */
 	}
 
 	/*
@@ -1477,6 +2588,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1484,7 +2596,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1496,16 +2607,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No skipping of non-required arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1569,6 +2674,8 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 	for (int ikey = sktrig; ikey < so->numberOfKeys; ikey++)
 	{
 		ScanKey		cur = so->keyData + ikey;
+		Datum		sk_argument = cur->sk_argument;
+		bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
 		Datum		tupdatum;
 		bool		tupnull;
 		int32		result;
@@ -1630,9 +2737,67 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_NEGPOSINF))
+		{
+			/* Just use the array's current array element */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											sk_argument, sk_isnull, cur);
+
+			/*
+			 * When scan key is marked NEXTPRIOR, the current array element is
+			 * "sk_argument + infinitesimal" (or the current array element is
+			 * "sk_argument - infinitesimal", during backwards scans)
+			 */
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXTPRIOR))
+			{
+				/*
+				 * tupdatum is actually still < "sk_argument + infinitesimal"
+				 * (or it's actually still > "sk_argument - infinitesimal")
+				 */
+				return true;
+			}
+		}
+		else
+		{
+			/*
+			 * The scankey lacks a conventional sk_argument/element value,
+			 * since it's marked as containing the sentinel value -inf/+inf.
+			 *
+			 * Note: -inf could mean "absolute" -inf, or it could represent
+			 * the lowest possible value that still satisfies the array's
+			 * low_compare.  +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * Compare tupdatum against -inf using array's low_compare, if any
+			 * (or compare it against +inf using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is > -inf sk_argument (or < +inf sk_argument).
+				 * It's time for caller to advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1964,18 +3129,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -2000,18 +3156,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2029,15 +3176,27 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * Skip array.  "Binary search" by checking if tupdatum/tupnull
+			 * are within the low_value/high_value range of the skip array.
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
+			Datum		sk_argument = cur->sk_argument;
+			bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
+
 			Assert(sktrig_required && required);
 
 			/*
@@ -2051,7 +3210,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 */
 			result = _bt_compare_array_skey(&so->orderProcs[ikey],
 											tupdatum, tupnull,
-											cur->sk_argument, cur);
+											sk_argument, sk_isnull, cur);
 		}
 
 		/*
@@ -2110,11 +3269,76 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+
+		if (!array)
+			continue;			/* no element to set in non-array */
+
+		/* Conventional arrays have a valid set_elem for us to advance to */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * Skip arrays generate array elements procedurally and on demand.
+		 * They "contain" elements for every possible datum from a given range
+		 * of values.  This is often the range -inf through to +inf.
+		 */
+		Assert(cur->sk_flags & SK_BT_SKIP);
+		Assert(array->num_elems == -1);
+		Assert(required);
+
+		/*
+		 * When a binary search of a conventional array locates a set_elem
+		 * that is merely the best available match for tupdatum (not an exact
+		 * match), set_elem isn't necessarily set to the absolute lowest or
+		 * highest array element (though we must set subsequent lower-order
+		 * !all_required_satisfied arrays that way, as the process cascades).
+		 *
+		 * However, when a "binary search" of a skip array finds that tupdatum
+		 * isn't within the range of the skip array, we always advance the
+		 * array to either the highest or the lowest possible element value
+		 * (it's often set to either the +inf or the -inf element/value).
+		 * There can be no "gaps between array elements", so either we find an
+		 * exact match or we follow the same steps followed for later arrays
+		 * that array advancement will cascade to.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * We need to set the array element to the final element in the
+			 * current scan direction for "beyond end of array element" array
+			 * advancement
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * The closest matching element is the lowest element; even that
+			 * still puts us ahead of caller's tuple in the key space
+			 */
+			Assert(sktrig < ikey);	/* Caller must get this right */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * Search found tupdatum within the range of the skip array.
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2465,6 +3689,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also cons up skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2588,8 +3814,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		inputsk = scan->keyData;
 
 	/*
-	 * Now that we have an estimate of the number of output scan keys,
-	 * allocate space for them
+	 * Now that we have an estimate of the number of output scan keys
+	 * (including any skip array scan keys), allocate space for them
 	 */
 	so->keyData = palloc(sizeof(ScanKeyData) * numberOfKeys);
 
@@ -2725,7 +3951,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
+						Assert(!array || array->num_elems > 0 ||
+							   array->num_elems == -1);
 						xform[j].skey = NULL;
 						xform[j].ikey = -1;
 					}
@@ -2888,7 +4115,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
+					Assert(!array || array->num_elems > 0 ||
+						   array->num_elems == -1);
 
 					/*
 					 * New key is more restrictive, and so replaces old key...
@@ -3030,10 +4258,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3108,6 +4337,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3181,6 +4426,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3744,6 +4990,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index edb09d4e3..e945686c8 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -96,6 +96,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 9c854e0e5..79658f068 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,49 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 date_finite(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index 8c6fc80c3..91682edd5 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -83,6 +83,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index bf42393be..0e6e9ebb9 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -6806,6 +6806,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		found_skip;
 	bool		found_saop;
 	bool		found_is_null_op;
 	double		num_sa_scans;
@@ -6831,6 +6832,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	found_skip = false;
 	found_saop = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
@@ -6839,15 +6841,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
 		ListCell   *lc2;
 
+		/*
+		 * XXX For now we just cost skip scans via generic rules: make a
+		 * uniform assumption that there will be 10 primitive index scans per
+		 * skipped attribute, relying on the "1/3 of all index pages" cap that
+		 * this costing has used since Postgres 17.  Also assume that skipping
+		 * won't take place for an index that has fewer than 100 pages.
+		 *
+		 * The current approach to costing leaves much to be desired, but is
+		 * at least better than nothing at all (keeping the code as it is on
+		 * HEAD just makes testing and review inconvenient).
+		 */
 		if (indexcol != iclause->indexcol)
 		{
 			/* Beginning of a new column's quals */
 			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			{
+				found_skip = true;	/* skip when no '=' qual for indexcol */
+				if (index->pages < 100)
+					break;
+				num_sa_scans += 10;
+			}
 			eqQualHere = false;
 			indexcol++;
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			{
+				/* no quals at all for indexcol */
+				found_skip = true;
+				if (index->pages < 100)
+					break;
+				num_sa_scans += 10 * (iclause->indexcol - indexcol);
+				continue;
+			}
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6920,6 +6945,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 45eb1b2fe..e2d98a62f 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -390,6 +393,70 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	*underflow = false;
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	*underflow = true;
+
+	return 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	*overflow = false;
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	*overflow = true;
+
+	return 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index c0a52cdcc..131b23a8e 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1753,6 +1754,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3587,6 +3599,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..9662fb2ba 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -583,6 +583,19 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure via an index <quote>skip scan</quote>.  The
+     APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..651f9323e 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intevening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner ill prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,10 +511,7 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
+   Multicolumn indexes should be used judiciously.  See
    <xref linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
@@ -669,9 +669,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 22d8ad1aa..63f03f3a7 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1056,7 +1063,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1069,7 +1077,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1082,7 +1091,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..8b6b775c1 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 3bbe4c5f9..a8d5be6c1 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5138,9 +5138,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..4246afefd 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 547d14b3e..3dffb3856 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2660,6 +2661,8 @@ SingleBoundSortItem
 SinglePartitionSpec
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

v5-0002-Refactor-handling-of-nbtree-array-redundancies.patchapplication/octet-stream; name=v5-0002-Refactor-handling-of-nbtree-array-redundancies.patchDownload
From 02143dea1394cf69c0ddd825d35491d776de9b82 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Thu, 8 Aug 2024 15:41:18 -0400
Subject: [PATCH v5 2/3] Refactor handling of nbtree array redundancies.

Rather than allocating memory for so.keyData[] at the start of each
btrescan, lazily allocate space later on, in _bt_preprocess_keys.  We
now allocate so.keyData[] after _bt_preprocess_array_keys is done
performing initial array related preprocessing.

An immediate benefit of this approach is that _bt_preprocess_array_keys
no longer needs to explicitly mark redundant array scan keys.  Other
code (_bt_preprocess_keys and its other subsidiary routines) no longer
have to interpret the scan key entries as redundant.  Redundant array
scan keys simply never appear in the _bt_preprocess_keys input array
(_bt_preprocess_array_keys removes them up front).

This refactoring is also preparation for an upcoming patch that will add
skip scan optimizations to nbtree.  _bt_preprocess_array_keys will be
taught to add new skip array scan keys to the _bt_preprocess_keys input
array (i.e. to arrayKeyData), so doing things this way avoids uselessly
palloc'ing so.keyData[], only to have to repalloc (to enlarge the array)
almost immediately afterwards.  This scheme allows _bt_preprocess_keys
to output a so.keyData[] scan key array that can be larger than the
original scan.keyData[] input array, due to the addition of skip array
scan keys within _bt_preprocess_array_keys.
---
 src/backend/access/nbtree/nbtree.c   |  10 +-
 src/backend/access/nbtree/nbtutils.c | 157 +++++++++++++--------------
 2 files changed, 83 insertions(+), 84 deletions(-)

diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index e5ce129cc..964f6c73a 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -324,11 +324,8 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	so = (BTScanOpaque) palloc(sizeof(BTScanOpaqueData));
 	BTScanPosInvalidate(so->currPos);
 	BTScanPosInvalidate(so->markPos);
-	if (scan->numberOfKeys > 0)
-		so->keyData = (ScanKey) palloc(scan->numberOfKeys * sizeof(ScanKeyData));
-	else
-		so->keyData = NULL;
 
+	so->keyData = NULL;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->oppoDirCheck = false;
@@ -410,6 +407,11 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 				scan->numberOfKeys * sizeof(ScanKeyData));
 	so->numberOfKeys = 0;		/* until _bt_preprocess_keys sets it */
 	so->numArrayKeys = 0;		/* ditto */
+
+	/* Release private storage allocated in previous btrescan, if any */
+	if (so->keyData != NULL)
+		pfree(so->keyData);
+	so->keyData = NULL;
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 1b39d8701..7fa977a62 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -62,7 +62,7 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
-static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan);
+static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
@@ -251,9 +251,6 @@ _bt_freestack(BTStack stack)
  * It is convenient for _bt_preprocess_keys caller to have to deal with no
  * more than one equality strategy array scan key per index attribute.  We'll
  * always be able to set things up that way when complete opfamilies are used.
- * Eliminated array scan keys can be recognized as those that have had their
- * sk_strategy field set to InvalidStrategy here by us.  Caller should avoid
- * including these in the scan's so->keyData[] output array.
  *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
@@ -261,18 +258,25 @@ _bt_freestack(BTStack stack)
  * preprocessing steps are complete.  This will convert the scan key offset
  * references into references to the scan's so->keyData[] output scan keys.
  *
+ * Caller must pass *numberOfKeys to give us a way to change the number of
+ * input scan keys (our output is caller's input).  The returned array can be
+ * smaller than scan->keyData[] when we eliminated a redundant array scan key
+ * (redundant with some other array scan key, for the same attribute).  Caller
+ * uses this to allocate so->keyData[] for the current btrescan.
+ *
  * Note: the reason we need to return a temp scan key array, rather than just
  * scribbling on scan->keyData, is that callers are permitted to call btrescan
  * without supplying a new set of scankey data.
  */
 static ScanKey
-_bt_preprocess_array_keys(IndexScanDesc scan)
+_bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
+	int			numArrayKeyData = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
-	int			numArrayKeys;
+	int			numArrayKeys,
+				output_ikey = 0;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -280,11 +284,11 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
+	Assert(scan->numberOfKeys);
 
 	/* Quick check to see if there are any array keys */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
 		cur = &scan->keyData[i];
 		if (cur->sk_flags & SK_SEARCHARRAY)
@@ -317,19 +321,18 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
-	/* Create modifiable copy of scan->keyData in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
-	memcpy(arrayKeyData, scan->keyData, numberOfKeys * sizeof(ScanKeyData));
+	/* Create output scan keys in the workspace context */
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
 	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -345,14 +348,21 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		int			num_nonnulls;
 		int			j;
 
-		cur = &arrayKeyData[i];
+		/*
+		 * Copy input scan key into temp arrayKeyData scan key array.  (From
+		 * here on, cur points at our copy of the input scan key.)
+		 */
+		cur = &arrayKeyData[output_ikey];
+		*cur = scan->keyData[input_ikey];
+
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
+		{
+			output_ikey++;		/* keep this non-array scan key */
 			continue;
+		}
 
 		/*
-		 * First, deconstruct the array into elements.  Anything allocated
-		 * here (including a possibly detoasted array value) is in the
-		 * workspace context.
+		 * Deconstruct the array into elements
 		 */
 		arrayval = DatumGetArrayTypeP(cur->sk_argument);
 		/* We could cache this data, but not clear it's worth it */
@@ -406,6 +416,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
+				output_ikey++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -416,6 +427,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
+				output_ikey++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -432,7 +444,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[i], &sortprocp);
+							&so->orderProcs[output_ikey], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -476,11 +488,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					break;
 				}
 
-				/*
-				 * Indicate to _bt_preprocess_keys caller that it must ignore
-				 * this scan key
-				 */
-				cur->sk_strategy = InvalidStrategy;
+				/* Throw away this scan key/array */
 				continue;
 			}
 
@@ -511,12 +519,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = i;
+		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
 		numArrayKeys++;
+		output_ikey++;			/* keep this scan key/array */
 	}
 
+	/* Set final number of arrayKeyData[] keys, array keys */
+	*numberOfKeys = output_ikey;
 	so->numArrayKeys = numArrayKeys;
 
 	MemoryContextSwitchTo(oldContext);
@@ -2429,10 +2440,12 @@ end_toplevel_scan:
 /*
  *	_bt_preprocess_keys() -- Preprocess scan keys
  *
+ * The first call here (per btrescan) allocates so->keyData[].
  * The given search-type keys (taken from scan->keyData[])
  * are copied to so->keyData[] with possible transformation.
  * scan->numberOfKeys is the number of input keys, so->numberOfKeys gets
- * the number of output keys (possibly less, never greater).
+ * the number of output keys.  Calling here a second or subsequent time
+ * (during the same btrescan) is a no-op.
  *
  * The output keys are marked with additional sk_flags bits beyond the
  * system-standard bits supplied by the caller.  The DESC and NULLS_FIRST
@@ -2519,9 +2532,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	int16	   *indoption = scan->indexRelation->rd_indoption;
 	int			new_numberOfKeys;
 	int			numberOfEqualCols;
-	ScanKey		inkeys;
-	ScanKey		outkeys;
-	ScanKey		cur;
+	ScanKey		inputsk;
 	BTScanKeyPreproc xform[BTMaxStrategyNumber];
 	bool		test_result;
 	int			i,
@@ -2553,7 +2564,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		return;					/* done if qual-less scan */
 
 	/* If any keys are SK_SEARCHARRAY type, set up array-key info */
-	arrayKeyData = _bt_preprocess_array_keys(scan);
+	arrayKeyData = _bt_preprocess_array_keys(scan, &numberOfKeys);
 	if (!so->qual_ok)
 	{
 		/* unmatchable array, so give up */
@@ -2567,32 +2578,36 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	 */
 	if (arrayKeyData)
 	{
-		inkeys = arrayKeyData;
+		inputsk = arrayKeyData;
 
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
 	}
 	else
-		inkeys = scan->keyData;
+		inputsk = scan->keyData;
+
+	/*
+	 * Now that we have an estimate of the number of output scan keys,
+	 * allocate space for them
+	 */
+	so->keyData = palloc(sizeof(ScanKeyData) * numberOfKeys);
 
-	outkeys = so->keyData;
-	cur = &inkeys[0];
 	/* we check that input keys are correctly ordered */
-	if (cur->sk_attno < 1)
+	if (inputsk[0].sk_attno < 1)
 		elog(ERROR, "btree index keys must be ordered by attribute");
 
 	/* We can short-circuit most of the work if there's just one key */
 	if (numberOfKeys == 1)
 	{
 		/* Apply indoption to scankey (might change sk_strategy!) */
-		if (!_bt_fix_scankey_strategy(cur, indoption))
+		if (!_bt_fix_scankey_strategy(inputsk, indoption))
 			so->qual_ok = false;
-		memcpy(outkeys, cur, sizeof(ScanKeyData));
+		memcpy(&so->keyData[0], &inputsk[0], sizeof(ScanKeyData));
 		so->numberOfKeys = 1;
 		/* We can mark the qual as required if it's for first index col */
-		if (cur->sk_attno == 1)
-			_bt_mark_scankey_required(outkeys);
+		if (inputsk[0].sk_attno == 1)
+			_bt_mark_scankey_required(&so->keyData[0]);
 		if (arrayKeyData)
 		{
 			/*
@@ -2600,8 +2615,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * (we'll miss out on the single value array transformation, but
 			 * that's not nearly as important when there's only one scan key)
 			 */
-			Assert(cur->sk_flags & SK_SEARCHARRAY);
-			Assert(cur->sk_strategy != BTEqualStrategyNumber ||
+			Assert(so->keyData[0].sk_flags & SK_SEARCHARRAY);
+			Assert(so->keyData[0].sk_strategy != BTEqualStrategyNumber ||
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
@@ -2629,12 +2644,12 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	 * handle after-last-key processing.  Actual exit from the loop is at the
 	 * "break" statement below.
 	 */
-	for (i = 0;; cur++, i++)
+	for (i = 0;; inputsk++, i++)
 	{
 		if (i < numberOfKeys)
 		{
 			/* Apply indoption to scankey (might change sk_strategy!) */
-			if (!_bt_fix_scankey_strategy(cur, indoption))
+			if (!_bt_fix_scankey_strategy(inputsk, indoption))
 			{
 				/* NULL can't be matched, so give up */
 				so->qual_ok = false;
@@ -2646,12 +2661,12 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		 * If we are at the end of the keys for a particular attr, finish up
 		 * processing and emit the cleaned-up keys.
 		 */
-		if (i == numberOfKeys || cur->sk_attno != attno)
+		if (i == numberOfKeys || inputsk->sk_attno != attno)
 		{
 			int			priorNumberOfEqualCols = numberOfEqualCols;
 
 			/* check input keys are correctly ordered */
-			if (i < numberOfKeys && cur->sk_attno < attno)
+			if (i < numberOfKeys && inputsk->sk_attno < attno)
 				elog(ERROR, "btree index keys must be ordered by attribute");
 
 			/*
@@ -2755,7 +2770,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			}
 
 			/*
-			 * Emit the cleaned-up keys into the outkeys[] array, and then
+			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
 			 */
@@ -2763,7 +2778,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			{
 				if (xform[j].skey)
 				{
-					ScanKey		outkey = &outkeys[new_numberOfKeys++];
+					ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
 					memcpy(outkey, xform[j].skey, sizeof(ScanKeyData));
 					if (arrayKeyData)
@@ -2780,19 +2795,19 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				break;
 
 			/* Re-initialize for new attno */
-			attno = cur->sk_attno;
+			attno = inputsk->sk_attno;
 			memset(xform, 0, sizeof(xform));
 		}
 
 		/* check strategy this key's operator corresponds to */
-		j = cur->sk_strategy - 1;
+		j = inputsk->sk_strategy - 1;
 
 		/* if row comparison, push it directly to the output array */
-		if (cur->sk_flags & SK_ROW_HEADER)
+		if (inputsk->sk_flags & SK_ROW_HEADER)
 		{
-			ScanKey		outkey = &outkeys[new_numberOfKeys++];
+			ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
-			memcpy(outkey, cur, sizeof(ScanKeyData));
+			memcpy(outkey, inputsk, sizeof(ScanKeyData));
 			if (arrayKeyData)
 				keyDataMap[new_numberOfKeys - 1] = i;
 			if (numberOfEqualCols == attno - 1)
@@ -2806,21 +2821,10 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			continue;
 		}
 
-		/*
-		 * Does this input scan key require further processing as an array?
-		 */
-		if (cur->sk_strategy == InvalidStrategy)
+		if (inputsk->sk_strategy == BTEqualStrategyNumber &&
+			(inputsk->sk_flags & SK_SEARCHARRAY))
 		{
-			/* _bt_preprocess_array_keys marked this array key redundant */
-			Assert(arrayKeyData);
-			Assert(cur->sk_flags & SK_SEARCHARRAY);
-			continue;
-		}
-
-		if (cur->sk_strategy == BTEqualStrategyNumber &&
-			(cur->sk_flags & SK_SEARCHARRAY))
-		{
-			/* _bt_preprocess_array_keys kept this array key */
+			/* maintain arrayidx for xform[] array */
 			Assert(arrayKeyData);
 			arrayidx++;
 		}
@@ -2832,7 +2836,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		if (xform[j].skey == NULL)
 		{
 			/* nope, so this scan key wins by default (at least for now) */
-			xform[j].skey = cur;
+			xform[j].skey = inputsk;
 			xform[j].ikey = i;
 			xform[j].arrayidx = arrayidx;
 		}
@@ -2850,7 +2854,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/*
 				 * Have to set up array keys
 				 */
-				if ((cur->sk_flags & SK_SEARCHARRAY))
+				if (inputsk->sk_flags & SK_SEARCHARRAY)
 				{
 					array = &so->arrayKeys[arrayidx - 1];
 					orderproc = so->orderProcs + i;
@@ -2878,7 +2882,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				 */
 			}
 
-			if (_bt_compare_scankey_args(scan, cur, cur, xform[j].skey,
+			if (_bt_compare_scankey_args(scan, inputsk, inputsk, xform[j].skey,
 										 array, orderproc, &test_result))
 			{
 				/* Have all we need to determine redundancy */
@@ -2892,7 +2896,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 					if (j != (BTEqualStrategyNumber - 1) ||
 						!(xform[j].skey->sk_flags & SK_SEARCHARRAY))
 					{
-						xform[j].skey = cur;
+						xform[j].skey = inputsk;
 						xform[j].ikey = i;
 						xform[j].arrayidx = arrayidx;
 					}
@@ -2905,7 +2909,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 						 * scan key.  _bt_compare_scankey_args expects us to
 						 * always keep arrays (and discard non-arrays).
 						 */
-						Assert(!(cur->sk_flags & SK_SEARCHARRAY));
+						Assert(!(inputsk->sk_flags & SK_SEARCHARRAY));
 					}
 				}
 				else if (j == (BTEqualStrategyNumber - 1))
@@ -2928,14 +2932,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				 * even with incomplete opfamilies.  _bt_advance_array_keys
 				 * depends on this.
 				 */
-				ScanKey		outkey = &outkeys[new_numberOfKeys++];
+				ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
 				memcpy(outkey, xform[j].skey, sizeof(ScanKeyData));
 				if (arrayKeyData)
 					keyDataMap[new_numberOfKeys - 1] = xform[j].ikey;
 				if (numberOfEqualCols == attno - 1)
 					_bt_mark_scankey_required(outkey);
-				xform[j].skey = cur;
+				xform[j].skey = inputsk;
 				xform[j].ikey = i;
 				xform[j].arrayidx = arrayidx;
 			}
@@ -3349,13 +3353,6 @@ _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption)
 		return true;
 	}
 
-	if (skey->sk_strategy == InvalidStrategy)
-	{
-		/* Already-eliminated array scan key; don't need to fix anything */
-		Assert(skey->sk_flags & SK_SEARCHARRAY);
-		return true;
-	}
-
 	/* Adjust strategy for DESC, if we didn't already */
 	if ((addflags & SK_BT_DESC) && !(skey->sk_flags & SK_BT_DESC))
 		skey->sk_strategy = BTCommuteStrategyNumber(skey->sk_strategy);
-- 
2.45.2

In reply to: Peter Geoghegan (#12)
4 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Mon, Jul 15, 2024 at 2:34 PM Peter Geoghegan <pg@bowt.ie> wrote:

On Fri, Jul 12, 2024 at 1:19 AM <Masahiro.Ikeda@nttdata.com> wrote:

I found the cost is estimated to much higher if the number of skipped attributes
is more than two. Is it expected behavior?

Yes and no.

Honestly, the current costing is just placeholder code. It is totally
inadequate. I'm not surprised that you found problems with it. I just
didn't put much work into it, because I didn't really know what to do.

Attached is v6, which finally does something sensible in btcostestimate.

v6 is also the first version that supports parallel index scans that
can skip. This works by extending the approach taken by scans with
regular SAOP arrays to work with skip arrays. We need to serialize and
deserialize the current array keys in shared memory, as datums -- we
cannot just use simple BTArrayKeyInfo.cur_elem offsets with skip
arrays.

v6 also includes the patch that shows "Index Searches" in EXPLAIN
ANALYZE output, just because it's convenient when testing the patch.
This has been independently submitted as
https://commitfest.postgresql.org/49/5183/, so probably doesn't need
review here.

v6 is the first version of the patch that is basically feature
complete. I only have one big open item left: I must still fix certain
regressions seen with queries that are very unfavorable for skip scan,
where the CPU cost (but not I/O cost) of maintaining skip arrays slows
things down. Overall, I'm making fast progress here.

Back to the topic of the btcostestimate/planner changes. The rest of
the email is a discussion of the cost model.

The planner changes probably still have some problems, but all of the
obvious problems have been fixed by v6. I found it useful to focus on
making the cost model not have any obvious problems instead of trying
to make it match a purely theoretical ideal. For example, your
(Ikeda-san's) complaint about the "Index Scan using idx_id1_id2_id3 on
public.test" test case having too high a cost (higher than the cost of
a slower sequential scan) has been fixed. It's now about 3x cheaper
than the sequential scan, since we're actually paying attention to
ndistinct in v6.

Just like when we cost SAOP arrays on HEAD, skip arrays are costed by
pessimistically multiplying together the estimated number of array
elements for all the scan's arrays, without trying to account for
correlation between index columns. Being pessimistic about
correlations like this is often wrong, but that still seems like the
best bias we could have, all things considered. Plus it's nothing new.

Range style skip arrays require a slightly more complicated approach
to estimating the number of array elements: costing applies a
selectivity estimate, taken from the associated index column's
inequality keys, and applies that estimate to ndistinct itself. That
way the cost of a range skip array is lower than an
otherwise-equivalent simple skip array case (we prorate ndistinct with
skip arrays). More importantly, the cost of more selectivity ranges is
lower than the cost of less selective ranges. There is also a bias
here: we don't account for skew in ndistinct. That's probably OK,
because at least it's a bias *against* skip scan.

The new cost model does not specifically try to account for how scans
will behave when no skipping should be expected at all -- cases where
a so-called "skip scan" degenerates into a full index scan. In theory,
we should be costing these scans the same as before, since there has
been no change in runtime behavior. Overall, the cost of full index
scans with very many distinct prefix column values goes down by quite
a bit -- the cost is something like 1/3 lower in typical cases.

The problem with preserving the cost model from HEAD for these
unfavorable cases for skip scan is that I don't feel that I understand
the existing behavior. In practice the revised costing seems to be a
somewhat more accurate predictor of the actual runtime of queries.
Another problem is that I can't see a good way to make the behavior
continuous when ndistinct starts small and grows so large that we
should expect a true full index scan. (As I mentioned at the start of
this email, there are unfixed regressions for these unfavorable cases,
so I'm basing this analysis on the "set skipscan_prefix_cols = 0"
behavior rather than the current default patch behavior to correct for
that. This behavior matches HEAD with a full index scan, and should
match the default behavior in a future version of the skip scan
patch.)

--
Peter Geoghegan

Attachments:

v6-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v6-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From 20e45ec5a4e9639173be625c4c4b87b86e870397 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v6 1/4] Show index search count in EXPLAIN ANALYZE.

Also stop counting the case where nbtree detects contradictory quals as
a distinct index search (do so neither in EXPLAIN ANALYZE nor in the
pg_stat_*_indexes.idx_scan stats).

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |  3 +
 src/backend/access/brin/brin.c                |  1 +
 src/backend/access/gin/ginscan.c              |  1 +
 src/backend/access/gist/gistget.c             |  2 +
 src/backend/access/hash/hashsearch.c          |  1 +
 src/backend/access/index/genam.c              |  1 +
 src/backend/access/nbtree/nbtree.c            | 11 ++++
 src/backend/access/nbtree/nbtsearch.c         |  9 ++-
 src/backend/access/spgist/spgscan.c           |  1 +
 src/backend/commands/explain.c                | 38 +++++++++++++
 doc/src/sgml/bloom.sgml                       |  6 +-
 doc/src/sgml/monitoring.sgml                  | 12 +++-
 doc/src/sgml/perform.sgml                     |  8 +++
 doc/src/sgml/ref/explain.sgml                 |  3 +-
 doc/src/sgml/rules.sgml                       |  1 +
 src/test/regress/expected/brin_multi.out      | 27 ++++++---
 src/test/regress/expected/memoize.out         | 50 +++++++++++-----
 src/test/regress/expected/partition_prune.out | 57 ++++++++++++++-----
 src/test/regress/expected/select.out          |  3 +-
 src/test/regress/sql/memoize.sql              |  6 +-
 src/test/regress/sql/partition_prune.sql      |  4 ++
 21 files changed, 198 insertions(+), 47 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 521043304..b992d4080 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -130,6 +130,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 6467bed60..749d8b845 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -581,6 +581,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index af24d3854..594478116 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index b35b8a975..36f1435cb 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index 0d99d6abc..927ba1039 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 43c95d610..5f4544724 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -116,6 +116,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 686a3206f..dfef6c12d 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -70,6 +70,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* instrumentation */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -551,6 +552,7 @@ btinitparallelscan(void *target)
 	SpinLockInit(&bt_target->btps_mutex);
 	bt_target->btps_scanPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -576,6 +578,7 @@ btparallelrescan(IndexScanDesc scan)
 	SpinLockAcquire(&btscan->btps_mutex);
 	btscan->btps_scanPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -680,6 +683,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			*pageno = btscan->btps_scanPage;
 			exit_loop = true;
@@ -752,6 +760,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -785,6 +795,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 	{
 		btscan->btps_scanPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 2551df8a6..4b91a192e 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -896,8 +896,6 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 
 	Assert(!BTScanPosIsValid(so->currPos));
 
-	pgstat_count_index_scan(rel);
-
 	/*
 	 * Examine the scan keys and eliminate any redundant keys; also mark the
 	 * keys that must be matched to continue the scan.
@@ -960,6 +958,13 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		_bt_start_array_keys(scan, dir);
 	}
 
+	/*
+	 * We've established that we'll either call _bt_search or _bt_endpoint.
+	 * Count this as a primitive index scan/index search.
+	 */
+	pgstat_count_index_scan(rel);
+	scan->nsearches++;
+
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
 	 *
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 03293a781..9138fc03a 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -423,6 +423,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 11df4a04d..31edc8684 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -89,6 +90,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -1975,6 +1977,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			if (es->analyze)
+				show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -1988,6 +1992,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			if (es->analyze)
+				show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2004,6 +2010,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			if (es->analyze)
+				show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2513,6 +2521,36 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc && scanDesc->nsearches > 0)
+		ExplainPropertyUInteger("Index Searches", NULL,
+								scanDesc->nsearches, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 19f2b172c..92b13f539 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -170,9 +170,10 @@ CREATE INDEX
    Heap Blocks: exact=28
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..1792.00 rows=2 width=0) (actual time=0.356..0.356 rows=29 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 0.408 ms
-(8 rows)
+(9 rows)
 </programlisting>
   </para>
 
@@ -202,11 +203,12 @@ CREATE INDEX
    -&gt;  BitmapAnd  (cost=24.34..24.34 rows=2 width=0) (actual time=0.027..0.027 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..12.04 rows=500 width=0) (actual time=0.026..0.026 rows=0 loops=1)
                Index Cond: (i5 = 123451)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
-(9 rows)
+(10 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 55417a6fa..487851994 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4077,12 +4077,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index ff689b652..1f2172960 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -702,8 +702,10 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Heap Blocks: exact=10
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 1
  Planning Time: 0.485 ms
  Execution Time: 0.073 ms
 </screen>
@@ -754,6 +756,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Heap Blocks: exact=90
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
  Planning Time: 0.187 ms
  Execution Time: 3.036 ms
 </screen>
@@ -819,6 +822,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -848,9 +852,11 @@ EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique
          Buffers: shared hit=4 read=3
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared hit=2
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
                Buffers: shared hit=2 read=3
  Planning:
    Buffers: shared hit=3
@@ -883,6 +889,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Heap Blocks: exact=90
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1017,6 +1024,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
  Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
  Planning Time: 0.077 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a854..e042638b7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -502,9 +502,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    Batches: 1  Memory Usage: 24kB
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(7 rows)
+(8 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index ae9ce9d8e..c24d56007 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 9ee09fe2f..1448179fb 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -312,7 +325,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(10 rows)
+               Index Searches: N
+(11 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -329,7 +343,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(10 rows)
+               Index Searches: N
+(11 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -349,6 +364,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -356,9 +372,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -366,8 +384,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -379,6 +398,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -387,11 +407,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7a03b4e36..18ea272b6 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2692,12 +2696,13 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+(53 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off)
@@ -2713,6 +2718,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
@@ -2741,7 +2747,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(38 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off)
@@ -2757,6 +2763,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
@@ -2787,7 +2794,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(40 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2865,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2885,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,8 +2974,10 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
@@ -2971,7 +2986,7 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
                Index Cond: (col1 < tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+(17 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2984,6 +2999,7 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
@@ -2992,7 +3008,7 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+(16 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3043,20 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 5
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 4
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+(18 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3050,15 +3069,17 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 3
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+(17 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3122,7 +3143,8 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
                Index Cond: (col1 > tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(16 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3482,12 +3504,14 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(15);
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3527,10 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(25);
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3578,16 @@ explain (analyze, costs off, summary off, timing off) select * from ma_test wher
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(17 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4157,17 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(18 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 33a6dceb0..b7cf35b9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index 2eaeb1477..9afe205e0 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 442428d93..085e746af 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

v6-0003-Refactor-handling-of-nbtree-array-redundancies.patchapplication/octet-stream; name=v6-0003-Refactor-handling-of-nbtree-array-redundancies.patchDownload
From 0e1c5d7e262d0930ef5f3d490de023a9ca00336f Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Thu, 8 Aug 2024 15:41:18 -0400
Subject: [PATCH v6 3/4] Refactor handling of nbtree array redundancies.

Rather than allocating memory for so.keyData[] at the start of each
btrescan, lazily allocate space later on, in _bt_preprocess_keys.  We
now allocate so.keyData[] after _bt_preprocess_array_keys is done
performing initial array related preprocessing.

An immediate benefit of this approach is that _bt_preprocess_array_keys
no longer needs to explicitly mark redundant array scan keys.  Other
code (_bt_preprocess_keys and its other subsidiary routines) no longer
have to interpret the scan key entries as redundant.  Redundant array
scan keys simply never appear in the _bt_preprocess_keys input array
(_bt_preprocess_array_keys removes them up front).

This refactoring is also preparation for an upcoming patch that will add
skip scan optimizations to nbtree.  _bt_preprocess_array_keys will be
taught to add new skip array scan keys to the _bt_preprocess_keys input
array (i.e. to arrayKeyData), so doing things this way avoids uselessly
palloc'ing so.keyData[], only to have to repalloc (to enlarge the array)
almost immediately afterwards.  This scheme allows _bt_preprocess_keys
to output a so.keyData[] scan key array that can be larger than the
original scan.keyData[] input array, due to the addition of skip array
scan keys within _bt_preprocess_array_keys.

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=9A_UtM7HzUThSkQ+BcrQsQZuNhWOvQWK06PRkEp=SKQ@mail.gmail.com
---
 src/backend/access/nbtree/nbtree.c   |  10 +-
 src/backend/access/nbtree/nbtutils.c | 156 +++++++++++++--------------
 2 files changed, 82 insertions(+), 84 deletions(-)

diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index e055571c8..689028dc5 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -325,11 +325,8 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	so = (BTScanOpaque) palloc(sizeof(BTScanOpaqueData));
 	BTScanPosInvalidate(so->currPos);
 	BTScanPosInvalidate(so->markPos);
-	if (scan->numberOfKeys > 0)
-		so->keyData = (ScanKey) palloc(scan->numberOfKeys * sizeof(ScanKeyData));
-	else
-		so->keyData = NULL;
 
+	so->keyData = NULL;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->oppoDirCheck = false;
@@ -411,6 +408,11 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 				scan->numberOfKeys * sizeof(ScanKeyData));
 	so->numberOfKeys = 0;		/* until _bt_preprocess_keys sets it */
 	so->numArrayKeys = 0;		/* ditto */
+
+	/* Release private storage allocated in previous btrescan, if any */
+	if (so->keyData != NULL)
+		pfree(so->keyData);
+	so->keyData = NULL;
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 98688a3d6..d1423bd85 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -62,7 +62,7 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
-static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan);
+static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
@@ -251,9 +251,6 @@ _bt_freestack(BTStack stack)
  * It is convenient for _bt_preprocess_keys caller to have to deal with no
  * more than one equality strategy array scan key per index attribute.  We'll
  * always be able to set things up that way when complete opfamilies are used.
- * Eliminated array scan keys can be recognized as those that have had their
- * sk_strategy field set to InvalidStrategy here by us.  Caller should avoid
- * including these in the scan's so->keyData[] output array.
  *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
@@ -261,18 +258,25 @@ _bt_freestack(BTStack stack)
  * preprocessing steps are complete.  This will convert the scan key offset
  * references into references to the scan's so->keyData[] output scan keys.
  *
+ * Caller must pass *numberOfKeys to give us a way to change the number of
+ * input scan keys (our output is caller's input).  The returned array can be
+ * smaller than scan->keyData[] when we eliminated a redundant array scan key
+ * (redundant with some other array scan key, for the same attribute).  Caller
+ * uses this to allocate so->keyData[] for the current btrescan.
+ *
  * Note: the reason we need to return a temp scan key array, rather than just
  * scribbling on scan->keyData, is that callers are permitted to call btrescan
  * without supplying a new set of scankey data.
  */
 static ScanKey
-_bt_preprocess_array_keys(IndexScanDesc scan)
+_bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
+	int			numArrayKeyData = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
-	int			numArrayKeys;
+	int			numArrayKeys,
+				output_ikey = 0;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -280,11 +284,11 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
+	Assert(scan->numberOfKeys);
 
 	/* Quick check to see if there are any array keys */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
 		cur = &scan->keyData[i];
 		if (cur->sk_flags & SK_SEARCHARRAY)
@@ -317,19 +321,18 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
-	/* Create modifiable copy of scan->keyData in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
-	memcpy(arrayKeyData, scan->keyData, numberOfKeys * sizeof(ScanKeyData));
+	/* Create output scan keys in the workspace context */
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
 	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -345,14 +348,20 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		int			num_nonnulls;
 		int			j;
 
-		cur = &arrayKeyData[i];
+		/*
+		 * Copy input scan key into temp arrayKeyData scan key array
+		 */
+		cur = &arrayKeyData[output_ikey];
+		*cur = scan->keyData[input_ikey];
+
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
+		{
+			output_ikey++;		/* keep this non-array scan key */
 			continue;
+		}
 
 		/*
-		 * First, deconstruct the array into elements.  Anything allocated
-		 * here (including a possibly detoasted array value) is in the
-		 * workspace context.
+		 * Deconstruct the array into elements
 		 */
 		arrayval = DatumGetArrayTypeP(cur->sk_argument);
 		/* We could cache this data, but not clear it's worth it */
@@ -406,6 +415,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
+				output_ikey++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -416,6 +426,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
+				output_ikey++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -432,7 +443,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[i], &sortprocp);
+							&so->orderProcs[output_ikey], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -476,11 +487,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					break;
 				}
 
-				/*
-				 * Indicate to _bt_preprocess_keys caller that it must ignore
-				 * this scan key
-				 */
-				cur->sk_strategy = InvalidStrategy;
+				/* Throw away this scan key/array */
 				continue;
 			}
 
@@ -511,12 +518,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = i;
+		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
 		numArrayKeys++;
+		output_ikey++;			/* keep this scan key/array */
 	}
 
+	/* Set final number of arrayKeyData[] keys, array keys */
+	*numberOfKeys = output_ikey;
 	so->numArrayKeys = numArrayKeys;
 
 	MemoryContextSwitchTo(oldContext);
@@ -2429,10 +2439,12 @@ end_toplevel_scan:
 /*
  *	_bt_preprocess_keys() -- Preprocess scan keys
  *
+ * The first call here (per btrescan) allocates so->keyData[].
  * The given search-type keys (taken from scan->keyData[])
  * are copied to so->keyData[] with possible transformation.
  * scan->numberOfKeys is the number of input keys, so->numberOfKeys gets
- * the number of output keys (possibly less, never greater).
+ * the number of output keys.  Calling here a second or subsequent time
+ * (during the same btrescan) is a no-op.
  *
  * The output keys are marked with additional sk_flags bits beyond the
  * system-standard bits supplied by the caller.  The DESC and NULLS_FIRST
@@ -2519,9 +2531,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	int16	   *indoption = scan->indexRelation->rd_indoption;
 	int			new_numberOfKeys;
 	int			numberOfEqualCols;
-	ScanKey		inkeys;
-	ScanKey		outkeys;
-	ScanKey		cur;
+	ScanKey		inputsk;
 	BTScanKeyPreproc xform[BTMaxStrategyNumber];
 	bool		test_result;
 	int			i,
@@ -2553,7 +2563,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		return;					/* done if qual-less scan */
 
 	/* If any keys are SK_SEARCHARRAY type, set up array-key info */
-	arrayKeyData = _bt_preprocess_array_keys(scan);
+	arrayKeyData = _bt_preprocess_array_keys(scan, &numberOfKeys);
 	if (!so->qual_ok)
 	{
 		/* unmatchable array, so give up */
@@ -2567,32 +2577,36 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	 */
 	if (arrayKeyData)
 	{
-		inkeys = arrayKeyData;
+		inputsk = arrayKeyData;
 
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
 	}
 	else
-		inkeys = scan->keyData;
+		inputsk = scan->keyData;
+
+	/*
+	 * Now that we have an estimate of the number of output scan keys,
+	 * allocate space for them
+	 */
+	so->keyData = palloc(sizeof(ScanKeyData) * numberOfKeys);
 
-	outkeys = so->keyData;
-	cur = &inkeys[0];
 	/* we check that input keys are correctly ordered */
-	if (cur->sk_attno < 1)
+	if (inputsk[0].sk_attno < 1)
 		elog(ERROR, "btree index keys must be ordered by attribute");
 
 	/* We can short-circuit most of the work if there's just one key */
 	if (numberOfKeys == 1)
 	{
 		/* Apply indoption to scankey (might change sk_strategy!) */
-		if (!_bt_fix_scankey_strategy(cur, indoption))
+		if (!_bt_fix_scankey_strategy(inputsk, indoption))
 			so->qual_ok = false;
-		memcpy(outkeys, cur, sizeof(ScanKeyData));
+		memcpy(&so->keyData[0], &inputsk[0], sizeof(ScanKeyData));
 		so->numberOfKeys = 1;
 		/* We can mark the qual as required if it's for first index col */
-		if (cur->sk_attno == 1)
-			_bt_mark_scankey_required(outkeys);
+		if (inputsk[0].sk_attno == 1)
+			_bt_mark_scankey_required(&so->keyData[0]);
 		if (arrayKeyData)
 		{
 			/*
@@ -2600,8 +2614,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * (we'll miss out on the single value array transformation, but
 			 * that's not nearly as important when there's only one scan key)
 			 */
-			Assert(cur->sk_flags & SK_SEARCHARRAY);
-			Assert(cur->sk_strategy != BTEqualStrategyNumber ||
+			Assert(so->keyData[0].sk_flags & SK_SEARCHARRAY);
+			Assert(so->keyData[0].sk_strategy != BTEqualStrategyNumber ||
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
@@ -2629,12 +2643,12 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	 * handle after-last-key processing.  Actual exit from the loop is at the
 	 * "break" statement below.
 	 */
-	for (i = 0;; cur++, i++)
+	for (i = 0;; inputsk++, i++)
 	{
 		if (i < numberOfKeys)
 		{
 			/* Apply indoption to scankey (might change sk_strategy!) */
-			if (!_bt_fix_scankey_strategy(cur, indoption))
+			if (!_bt_fix_scankey_strategy(inputsk, indoption))
 			{
 				/* NULL can't be matched, so give up */
 				so->qual_ok = false;
@@ -2646,12 +2660,12 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		 * If we are at the end of the keys for a particular attr, finish up
 		 * processing and emit the cleaned-up keys.
 		 */
-		if (i == numberOfKeys || cur->sk_attno != attno)
+		if (i == numberOfKeys || inputsk->sk_attno != attno)
 		{
 			int			priorNumberOfEqualCols = numberOfEqualCols;
 
 			/* check input keys are correctly ordered */
-			if (i < numberOfKeys && cur->sk_attno < attno)
+			if (i < numberOfKeys && inputsk->sk_attno < attno)
 				elog(ERROR, "btree index keys must be ordered by attribute");
 
 			/*
@@ -2755,7 +2769,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			}
 
 			/*
-			 * Emit the cleaned-up keys into the outkeys[] array, and then
+			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
 			 */
@@ -2763,7 +2777,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			{
 				if (xform[j].skey)
 				{
-					ScanKey		outkey = &outkeys[new_numberOfKeys++];
+					ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
 					memcpy(outkey, xform[j].skey, sizeof(ScanKeyData));
 					if (arrayKeyData)
@@ -2780,19 +2794,19 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				break;
 
 			/* Re-initialize for new attno */
-			attno = cur->sk_attno;
+			attno = inputsk->sk_attno;
 			memset(xform, 0, sizeof(xform));
 		}
 
 		/* check strategy this key's operator corresponds to */
-		j = cur->sk_strategy - 1;
+		j = inputsk->sk_strategy - 1;
 
 		/* if row comparison, push it directly to the output array */
-		if (cur->sk_flags & SK_ROW_HEADER)
+		if (inputsk->sk_flags & SK_ROW_HEADER)
 		{
-			ScanKey		outkey = &outkeys[new_numberOfKeys++];
+			ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
-			memcpy(outkey, cur, sizeof(ScanKeyData));
+			memcpy(outkey, inputsk, sizeof(ScanKeyData));
 			if (arrayKeyData)
 				keyDataMap[new_numberOfKeys - 1] = i;
 			if (numberOfEqualCols == attno - 1)
@@ -2806,21 +2820,10 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			continue;
 		}
 
-		/*
-		 * Does this input scan key require further processing as an array?
-		 */
-		if (cur->sk_strategy == InvalidStrategy)
+		if (inputsk->sk_strategy == BTEqualStrategyNumber &&
+			(inputsk->sk_flags & SK_SEARCHARRAY))
 		{
-			/* _bt_preprocess_array_keys marked this array key redundant */
-			Assert(arrayKeyData);
-			Assert(cur->sk_flags & SK_SEARCHARRAY);
-			continue;
-		}
-
-		if (cur->sk_strategy == BTEqualStrategyNumber &&
-			(cur->sk_flags & SK_SEARCHARRAY))
-		{
-			/* _bt_preprocess_array_keys kept this array key */
+			/* maintain arrayidx for xform[] array */
 			Assert(arrayKeyData);
 			arrayidx++;
 		}
@@ -2832,7 +2835,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		if (xform[j].skey == NULL)
 		{
 			/* nope, so this scan key wins by default (at least for now) */
-			xform[j].skey = cur;
+			xform[j].skey = inputsk;
 			xform[j].ikey = i;
 			xform[j].arrayidx = arrayidx;
 		}
@@ -2850,7 +2853,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/*
 				 * Have to set up array keys
 				 */
-				if ((cur->sk_flags & SK_SEARCHARRAY))
+				if (inputsk->sk_flags & SK_SEARCHARRAY)
 				{
 					array = &so->arrayKeys[arrayidx - 1];
 					orderproc = so->orderProcs + i;
@@ -2878,7 +2881,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				 */
 			}
 
-			if (_bt_compare_scankey_args(scan, cur, cur, xform[j].skey,
+			if (_bt_compare_scankey_args(scan, inputsk, inputsk, xform[j].skey,
 										 array, orderproc, &test_result))
 			{
 				/* Have all we need to determine redundancy */
@@ -2892,7 +2895,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 					if (j != (BTEqualStrategyNumber - 1) ||
 						!(xform[j].skey->sk_flags & SK_SEARCHARRAY))
 					{
-						xform[j].skey = cur;
+						xform[j].skey = inputsk;
 						xform[j].ikey = i;
 						xform[j].arrayidx = arrayidx;
 					}
@@ -2905,7 +2908,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 						 * scan key.  _bt_compare_scankey_args expects us to
 						 * always keep arrays (and discard non-arrays).
 						 */
-						Assert(!(cur->sk_flags & SK_SEARCHARRAY));
+						Assert(!(inputsk->sk_flags & SK_SEARCHARRAY));
 					}
 				}
 				else if (j == (BTEqualStrategyNumber - 1))
@@ -2928,14 +2931,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				 * even with incomplete opfamilies.  _bt_advance_array_keys
 				 * depends on this.
 				 */
-				ScanKey		outkey = &outkeys[new_numberOfKeys++];
+				ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
 				memcpy(outkey, xform[j].skey, sizeof(ScanKeyData));
 				if (arrayKeyData)
 					keyDataMap[new_numberOfKeys - 1] = xform[j].ikey;
 				if (numberOfEqualCols == attno - 1)
 					_bt_mark_scankey_required(outkey);
-				xform[j].skey = cur;
+				xform[j].skey = inputsk;
 				xform[j].ikey = i;
 				xform[j].arrayidx = arrayidx;
 			}
@@ -3349,13 +3352,6 @@ _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption)
 		return true;
 	}
 
-	if (skey->sk_strategy == InvalidStrategy)
-	{
-		/* Already-eliminated array scan key; don't need to fix anything */
-		Assert(skey->sk_flags & SK_SEARCHARRAY);
-		return true;
-	}
-
 	/* Adjust strategy for DESC, if we didn't already */
 	if ((addflags & SK_BT_DESC) && !(skey->sk_flags & SK_BT_DESC))
 		skey->sk_strategy = BTCommuteStrategyNumber(skey->sk_strategy);
-- 
2.45.2

v6-0002-Normalize-nbtree-truncated-high-key-array-behavio.patchapplication/octet-stream; name=v6-0002-Normalize-nbtree-truncated-high-key-array-behavio.patchDownload
From 44e64c24e6ba073a2f97cef15ae49281c2046e86 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Thu, 8 Aug 2024 13:51:18 -0400
Subject: [PATCH v6 2/4] Normalize nbtree truncated high key array behavior.

Commit 5bf748b8 taught nbtree ScalarArrayOp array processing to decide
when and how to start the next primitive index scan based on physical
index characteristics.  This included rules for deciding whether to
start a new primitive index scan (or whether to move onto the right
sibling leaf page instead) whenever the scan encounters a leaf high key
with truncated lower-order columns whose omitted/-inf values are covered
by one or more arrays.

Prior to this commit, nbtree would treat a truncated column as
satisfying a scan key that marked required in the current scan
direction.  It would just give up and start a new primitive index scan
in cases involving inequalities required in the opposite direction only
(in practice this meant > and >= strategy scan keys, since only forward
scans consider the page high key like this).

Bring > and >= strategy scan keys in line with other required scan key
types: have nbtree persist with its current primitive index scan
regardless of the operator strategy in use.  This requires scheduling
and then performing an explicit check of the next page's high key (if
any) at the point that _bt_readpage is next called.

Although this could be considered a stand alone piece of work, it's
mostly intended as preparation for an upcoming patch that adds skip scan
optimizations to nbtree.  Without this work there are cases where the
scan's skip arrays trigger an excessive number of primitive index scans
due to most high keys having a truncated attribute that was previously
treated as not satisfying a required > or >= strategy scan key.

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=9A_UtM7HzUThSkQ+BcrQsQZuNhWOvQWK06PRkEp=SKQ@mail.gmail.com
---
 src/include/access/nbtree.h           |   3 +
 src/backend/access/nbtree/nbtree.c    |   4 +
 src/backend/access/nbtree/nbtsearch.c |  22 +++++
 src/backend/access/nbtree/nbtutils.c  | 119 ++++++++++++++------------
 4 files changed, 95 insertions(+), 53 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 9af9b3ecd..f578cdb73 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1048,6 +1048,7 @@ typedef struct BTScanOpaqueData
 	int			numArrayKeys;	/* number of equality-type array keys */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Last array advancement matched -inf attr? */
+	bool		oppoDirCheck;	/* check opposite dir scan keys? */
 	BTArrayKeyInfo *arrayKeys;	/* info about each equality-type array key */
 	FmgrInfo   *orderProcs;		/* ORDER procs for required equality keys */
 	MemoryContext arrayContext; /* scan-lifespan context for array data */
@@ -1288,6 +1289,8 @@ extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir);
 extern void _bt_preprocess_keys(IndexScanDesc scan);
 extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 						  IndexTuple tuple, int tupnatts);
+extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+								  IndexTuple finaltup);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index dfef6c12d..e055571c8 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -332,6 +332,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 
 	so->needPrimScan = false;
 	so->scanBehind = false;
+	so->oppoDirCheck = false;
 	so->arrayKeys = NULL;
 	so->orderProcs = NULL;
 	so->arrayContext = NULL;
@@ -375,6 +376,7 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 	so->markItemIndex = -1;
 	so->needPrimScan = false;
 	so->scanBehind = false;
+	so->oppoDirCheck = false;
 	BTScanPosUnpinIfPinned(so->markPos);
 	BTScanPosInvalidate(so->markPos);
 
@@ -629,6 +631,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 		 */
 		so->needPrimScan = false;
 		so->scanBehind = false;
+		so->oppoDirCheck = false;
 	}
 	else
 	{
@@ -673,6 +676,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 				}
 				so->needPrimScan = true;
 				so->scanBehind = false;
+				so->oppoDirCheck = false;
 				*pageno = InvalidBlockNumber;
 				exit_loop = true;
 			}
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 4b91a192e..e5f941e0a 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1704,6 +1704,28 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			ItemId		iid = PageGetItemId(page, P_HIKEY);
 
 			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+			if (unlikely(so->oppoDirCheck))
+			{
+				/*
+				 * Last _bt_readpage call scheduled precheck of finaltup for
+				 * required scan keys up to and including a > or >= scan key
+				 * (necessary because > and >= are only generally considered
+				 * required when scanning backwards)
+				 */
+				Assert(so->scanBehind);
+				so->oppoDirCheck = false;
+				if (!_bt_oppodir_checkkeys(scan, dir, pstate.finaltup))
+				{
+					/*
+					 * Back out of continuing with this leaf page -- schedule
+					 * another primitive index scan after all
+					 */
+					so->currPos.moreRight = false;
+					so->needPrimScan = true;
+					return false;
+				}
+			}
 		}
 
 		/* load items[] in ascending order */
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index c22ccec78..98688a3d6 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -1362,7 +1362,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 			curArrayKey->cur_elem = 0;
 		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
 	}
-	so->scanBehind = false;
+	so->scanBehind = so->oppoDirCheck = false;	/* reset */
 }
 
 /*
@@ -1671,8 +1671,7 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
 
 	Assert(so->numArrayKeys);
 
-	/* scanBehind flag doesn't persist across primitive index scans - reset */
-	so->scanBehind = false;
+	so->scanBehind = so->oppoDirCheck = false;	/* reset */
 
 	/*
 	 * Array keys are advanced within _bt_checkkeys when the scan reaches the
@@ -1808,7 +1807,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 
-		so->scanBehind = false; /* reset */
+		so->scanBehind = so->oppoDirCheck = false;	/* reset */
 
 		/*
 		 * Required scan key wasn't satisfied, so required arrays will have to
@@ -2293,19 +2292,27 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	if (so->scanBehind && has_required_opposite_direction_only)
 	{
 		/*
-		 * However, we avoid this behavior whenever the scan involves a scan
+		 * However, we do things differently whenever the scan involves a scan
 		 * key required in the opposite direction to the scan only, along with
 		 * a finaltup with at least one truncated attribute that's associated
 		 * with a scan key marked required (required in either direction).
 		 *
 		 * _bt_check_compare simply won't stop the scan for a scan key that's
 		 * marked required in the opposite scan direction only.  That leaves
-		 * us without any reliable way of reconsidering any opposite-direction
+		 * us without an automatic way of reconsidering any opposite-direction
 		 * inequalities if it turns out that starting a new primitive index
 		 * scan will allow _bt_first to skip ahead by a great many leaf pages
 		 * (see next section for details of how that works).
+		 *
+		 * We deal with this by explicitly scheduling a finaltup recheck for
+		 * the next page -- we'll call _bt_oppodir_checkkeys for the next
+		 * page's finaltup instead.  You can think of this as a way of dealing
+		 * with this page's finaltup being truncated by checking the next
+		 * page's finaltup instead.  And you can think of the oppoDirCheck
+		 * recheck handling within _bt_readpage as complementing the similar
+		 * scanBehind recheck made from within _bt_checkkeys.
 		 */
-		goto new_prim_scan;
+		so->oppoDirCheck = true;	/* schedule next page's finaltup recheck */
 	}
 
 	/*
@@ -2343,54 +2350,16 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * also before the _bt_first-wise start of tuples for our new qual.  That
 	 * at least suggests many more skippable pages beyond the current page.
 	 */
-	if (has_required_opposite_direction_only && pstate->finaltup &&
-		(all_required_satisfied || oppodir_inequality_sktrig))
+	else if (has_required_opposite_direction_only && pstate->finaltup &&
+			 (all_required_satisfied || oppodir_inequality_sktrig) &&
+			 unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup)))
 	{
-		int			nfinaltupatts = BTreeTupleGetNAtts(pstate->finaltup, rel);
-		ScanDirection flipped;
-		bool		continuescanflip;
-		int			opsktrig;
-
 		/*
-		 * We're checking finaltup (which is usually not caller's tuple), so
-		 * cannot reuse work from caller's earlier _bt_check_compare call.
-		 *
-		 * Flip the scan direction when calling _bt_check_compare this time,
-		 * so that it will set continuescanflip=false when it encounters an
-		 * inequality required in the opposite scan direction.
+		 * Make sure that any non-required arrays are set to the first array
+		 * element for the current scan direction
 		 */
-		Assert(!so->scanBehind);
-		opsktrig = 0;
-		flipped = -dir;
-		_bt_check_compare(scan, flipped,
-						  pstate->finaltup, nfinaltupatts, tupdesc,
-						  false, false, false,
-						  &continuescanflip, &opsktrig);
-
-		/*
-		 * Only start a new primitive index scan when finaltup has a required
-		 * unsatisfied inequality (unsatisfied in the opposite direction)
-		 */
-		Assert(all_required_satisfied != oppodir_inequality_sktrig);
-		if (unlikely(!continuescanflip &&
-					 so->keyData[opsktrig].sk_strategy != BTEqualStrategyNumber))
-		{
-			/*
-			 * It's possible for the same inequality to be unsatisfied by both
-			 * caller's tuple (in scan's direction) and finaltup (in the
-			 * opposite direction) due to _bt_check_compare's behavior with
-			 * NULLs
-			 */
-			Assert(opsktrig >= sktrig); /* not opsktrig > sktrig due to NULLs */
-
-			/*
-			 * Make sure that any non-required arrays are set to the first
-			 * array element for the current scan direction
-			 */
-			_bt_rewind_nonrequired_arrays(scan, dir);
-
-			goto new_prim_scan;
-		}
+		_bt_rewind_nonrequired_arrays(scan, dir);
+		goto new_prim_scan;
 	}
 
 	/*
@@ -3522,7 +3491,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 *
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
-		Assert(!so->scanBehind && !pstate->prechecked && !pstate->firstmatch);
+		Assert(!so->scanBehind && !so->oppoDirCheck);
+		Assert(!pstate->prechecked && !pstate->firstmatch);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
@@ -3634,6 +3604,49 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 								  ikey, true);
 }
 
+/*
+ * Test whether an indextuple satisfies inequalities required in the opposite
+ * direction only (and lower-order equalities required in either direction).
+ *
+ * scan: index scan descriptor (containing a search-type scankey)
+ * dir: current scan direction (flipped by us to get opposite direction)
+ * finaltup: final index tuple on the page
+ *
+ * Caller's finaltup tuple is the page high key (for forwards scans), or the
+ * first non-pivot tuple (for backwards scans).  Caller during scans with
+ * required array keys.
+ *
+ * Return true if finatup satisfies keys, false if not.  If the tuple fails to
+ * pass the qual, then caller is should start another primitive index scan;
+ * _bt_first can efficiently relocate the scan to a far later leaf page.
+ *
+ * Note: we focus on required-in-opposite-direction scan keys (e.g. for a
+ * required > or >= key, assuming a forwards scan) because _bt_checkkeys() can
+ * always deal with required-in-current-direction scan keys on its own.
+ */
+bool
+_bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+					  IndexTuple finaltup)
+{
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	int			nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+	bool		continuescan;
+	ScanDirection flipped = -dir;
+	int			ikey = 0;
+
+	Assert(so->numArrayKeys);
+
+	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
+					  false, false, false, &continuescan, &ikey);
+
+	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
+		return false;
+
+	return true;
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
-- 
2.45.2

v6-0004-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v6-0004-Add-skip-scan-to-nbtree.patchDownload
From 7d4f52c26f91da587d20b84321501065025ddd5a Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v6 4/4] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
useful in cases where the total number of distinct values in the column
'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.

Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXTPRIOR scan key flag, without directly changing its sk_argument.
The presence of NEXTPRIOR makes the scan interpret the key's sk_argument
as coming immediately after (or coming immediately before) sk_argument
in the key space.  The key value must still come before (or still come
after) any possible greater-than (or less-than) indexable/non-sentinel
value.  Obviously, the scan will never locate any exactly equal tuples.
But attempting to locate a match serves to make the scan locate the true
next value in whatever way it determines is most efficient, without any
need for special cases in high level scan-related code.  In particular,
this design obviates the need for explicit "next key" index probes.

Though it's typical for nbtree preprocessing to cons up skip arrays when
it will allow the scan to apply one or more omitted-from-query leading
key columns when skipping, that's never a requirement.  There are hardly
any limitations around where skip arrays/scan keys may appear relative
to conventional/input scan keys.  This is no less true in the presence
of conventional SAOP array scan keys, which may both roll over and be
rolled over by skip arrays.  For example, a skip array on the column "b"
is generated with quals such as "WHERE a = 42 AND c IN (1, 2, 3)".  As
with any nbtree scan involving arrays, whether or not we actually skip
depends on the physical characteristics of the index during the scan.

The optimizer doesn't use distinct new index paths to represent index
skip scans.  Skipping isn't an either/or question.  It's possible for
individual index scans to conspicuously vary how and when they skip in
order to deal with variation in how leading column values cluster
together over the key space of the index.  A dynamic strategy seems to
work best.  Skipping can be used during nbtree bitmap index scans,
nbtree index scans, and nbtree index-only scans.  Parallel index skip
scan is also supported.

Preprocessing will never cons up a skip array for an index column that
has an equality strategy scan key on input, but will do so for indexed
columns that are only constrained by inequality type input scan keys.
This allows the scan to skip using a range skip array.  These are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".
Such transformations only happen when they enable later preprocessing to
mark the copied-from-input scan key on "b" required to continue the scan
(otherwise, preprocessing directly outputs the >= and <= keys on "a" in
the traditional way, without adding a superseding skip array on "a").

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   27 +-
 src/include/catalog/pg_amproc.dat             |   16 +
 src/include/catalog/pg_proc.dat               |   24 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  109 ++
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  261 +++
 src/backend/access/nbtree/nbtree.c            |  218 ++-
 src/backend/access/nbtree/nbtsearch.c         |   93 +-
 src/backend/access/nbtree/nbtutils.c          | 1418 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   44 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  368 +++--
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/uuid.c                  |   67 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/btree.sgml                       |   13 +
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   40 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |    6 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   61 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    2 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 34 files changed, 2630 insertions(+), 308 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index f25c9d58a..651843b4e 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -195,7 +195,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index f578cdb73..7271c7033 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1031,10 +1033,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard arrays that store elements in memory */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup set to valid routine? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* opclass skip scan support, when use_sksup */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo	order_low;		/* low_compare's ORDER proc */
+	FmgrInfo	order_high;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1124,6 +1138,9 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array, for skip scan */
+#define SK_BT_NEGPOSINF	0x00080000	/* no sk_argument, -inf/+inf key */
+#define SK_BT_NEXTPRIOR	0x00100000	/* sk_argument is next/prior key */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1160,6 +1177,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1170,7 +1191,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index f639c3a6a..2a8f6f3f1 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -122,6 +128,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +149,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +170,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +205,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +275,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 85f42be1b..17b089fa3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2214,6 +2229,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4401,6 +4419,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -9239,6 +9260,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d70e6d37e..5b58739ad 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..d91390fc6
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,109 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * This happens at the point where the scan determines that another primitive
+ * index scan is required.  The next value is used (in combination with at
+ * least one additional lower-order non-skip key, taken from the SQL query) to
+ * relocate the scan, skipping over many irrelevant leaf pages in the process.
+ *
+ * Skip support generally works best with discrete types such as integer,
+ * date, and boolean; types where there is a decent chance that indexes will
+ * contain contiguous values (given a leading attributes using the opclass).
+ * When gaps/discontinuities are naturally rare (e.g., a leading identity
+ * column in a composite index, a date column preceding a product_id column),
+ * then it makes sense for skip scans to optimistically assume that the next
+ * distinct indexable value will find directly matching index tuples.
+ *
+ * The B-Tree code can fall back on next-key sentinel values for any opclass
+ * that doesn't provide its own skip support function.  There is no point in
+ * providing skip support unless the next indexed key value is often the next
+ * indexable value (at least with some workloads).  Opclasses where that never
+ * works out in practice should just rely on the B-Tree AM's generic next-key
+ * fallback strategy.  Opclasses where adding skip support is infeasible or
+ * hard (e.g., an opclass for a continuous type) can also use the fallback.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called.
+ * If an opclass can only set some of the fields, then it cannot safely
+ * provide a skip support routine.
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming standard ascending
+	 * order).  This helps the B-Tree code with finding its initial position
+	 * at the leaf level (during the skip scan's first primitive index scan).
+	 * In other words, it gives the B-Tree code a useful value to start from,
+	 * before any data has been read from the index.
+	 *
+	 * low_elem and high_elem are also used by skip scans to determine when
+	 * they've reached the final possible value (in the current direction).
+	 * It's typical for the scan to run out of leaf pages before it runs out
+	 * of unscanned indexable values, but it's still useful for the scan to
+	 * have a way to recognize when it has reached the last possible value
+	 * (this saves us a useless probe that just lands on the final leaf page).
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (in the case of pass-by-reference
+	 * types).  It's not okay for these functions to leak any memory.
+	 *
+	 * Both decrement and increment callbacks are guaranteed to never be
+	 * called with a NULL "existing" arg.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is undefined, and the B-Tree
+	 * code is entitled to assume that no memory will have been allocated.
+	 *
+	 * The B-Tree skip scan caller's "existing" datum is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must be liberal
+	 * in accepting every possible representational variation within the
+	 * underlying data type.  On the other hand, opclasses are _not_ expected
+	 * to preserve any information that doesn't affect how datums are sorted
+	 * (e.g., skip support for a fixed precision numeric type isn't required
+	 * to preserve datum display scale).
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index dcd04b813..dc99dad29 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..deb387453 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,49 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +149,49 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +215,49 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +301,49 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +465,49 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +541,48 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 689028dc5..d318a1d88 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -32,6 +32,7 @@
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
 #include "storage/smgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -71,7 +72,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* instrumentation */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -79,11 +80,21 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The reset of the space allocated in shared memory is also used when
+	 * scans need to schedule another primitive index scan.  It holds a
+	 * flattened representation of the backend's skip array datums, if any.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -539,10 +550,155 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
 	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Assume every index attribute might require that we generate a skip scan
+	 * key
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		Form_pg_attribute attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/* Restore skip array */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+
+		/* Now that old sk_argument memory is freed, copy over sk_flags */
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument to serialize */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -553,7 +709,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_scanPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	bt_target->btps_nsearches = 0;
@@ -575,15 +732,15 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_scanPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -611,6 +768,7 @@ btparallelrescan(IndexScanDesc scan)
 bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false;
 	bool		status = true;
@@ -650,7 +808,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -668,14 +826,10 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			if (first)
 			{
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
+
 				so->needPrimScan = true;
 				so->scanBehind = false;
 				so->oppoDirCheck = false;
@@ -698,7 +852,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			*pageno = btscan->btps_scanPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -728,10 +882,10 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber scan_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_scanPage = scan_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -745,6 +899,7 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber scan_page)
 void
 _bt_parallel_done(IndexScanDesc scan)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
 	bool		status_changed = false;
@@ -753,6 +908,17 @@ _bt_parallel_done(IndexScanDesc scan)
 	if (parallel_scan == NULL)
 		return;
 
+	/*
+	 * Defensively disallow marking parallel scan done when this backend has a
+	 * pending primitive index scan
+	 *
+	 * XXX Might be better to remove the call here made by _bt_first right
+	 * after _bt_endpoint is called...since we don't have a similar call after
+	 * _bt_search is called.
+	 */
+	if (so->needPrimScan)
+		return;
+
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
@@ -760,7 +926,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
@@ -768,7 +934,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -786,6 +952,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -795,7 +962,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_scanPage == prev_scan_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -804,14 +971,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index e5f941e0a..ed5593c62 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -883,7 +883,6 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	Buffer		buf;
 	BTStack		stack;
 	OffsetNumber offnum;
-	StrategyNumber strat;
 	BTScanInsertData inskey;
 	ScanKey		startKeys[INDEX_MAX_KEYS];
 	ScanKeyData notnullkeys[INDEX_MAX_KEYS];
@@ -975,7 +974,20 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * a > or < boundary or find an attribute with no boundary (which can be
 	 * thought of as the same as "> -infinity"), we can't use keys for any
 	 * attributes to its right, because it would break our simplistic notion
-	 * of what initial positioning strategy to use.
+	 * of what initial positioning strategy to use.  In practice skip scan
+	 * typically enables us to use all scan keys here, even with a set of
+	 * input keys that leave a "gap" between two index attributes (cases with
+	 * multiple gaps will even manage this without any special restrictions).
+	 *
+	 * Skip scan works by having _bt_preprocess_keys cons up = boundary keys
+	 * for any index columns that were missing a = key in scan->keyData[], the
+	 * input scan keys passed to us by the executor.  This happens for index
+	 * attributes prior to the attribute of our final input scan key.  The
+	 * underlying = keys use skip arrays.  The keys can be thought of as the
+	 * same as "col = ANY('{every possible col value}')".  Note that this
+	 * often includes the array element NULL, which the scan will treat as an
+	 * IS NULL qual (the skip array's scan key is already marked SK_SEARCHNULL
+	 * when we're called, so we need no special handling for this case here).
 	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
@@ -1050,6 +1062,47 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		{
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					/* -inf/+inf element from a skip array's scan key */
+					ScanKey		origchosen = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == chosen - so->keyData)
+							break;
+					}
+
+					/* use array's inequality key in startKeys[] */
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					Assert(!chosen ||
+						   chosen->sk_attno == origchosen->sk_attno);
+
+					if (!array->null_elem)
+					{
+						/*
+						 * The array does not include a NULL element (meaning
+						 * array advancement never generates an IS NULL qual).
+						 * We'll deduce a NOT NULL key to skip over any NULLs
+						 * when there's no usable low_compare (or no usable
+						 * high_compare, during a backwards scan).
+						 *
+						 * Note: this also handles an explicit NOT NULL key
+						 * that preprocessing folded into the skip array (it
+						 * doesn't save them in low_compare/high_compare).
+						 */
+						impliesNN = origchosen;
+					}
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
 				/*
 				 * Done looking at keys for curattr.  If we didn't find a
 				 * usable boundary key, see if we can deduce a NOT NULL key.
@@ -1083,16 +1136,42 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 				startKeys[keysz++] = chosen;
 
+				if (chosen->sk_flags & SK_BT_NEXTPRIOR)
+				{
+					/*
+					 * Next/prior key element from a skip array's scan key.
+					 * 'chosen' could be SK_ISNULL, in which case startKeys[]
+					 * positions us at the first tuple > NULL (for backwards
+					 * scans it's the first tuple < NULL instead).
+					 *
+					 * Adjust strat_total, so that our = key gets treated like
+					 * a > key (or like a < key) within _bt_search.
+					 */
+					Assert(strat_total == BTEqualStrategyNumber);
+					if (ScanDirectionIsForward(dir))
+						strat_total = BTGreaterStrategyNumber;
+					else
+						strat_total = BTLessStrategyNumber;
+
+					/*
+					 * We'll never find an exact = match for a NEXTPRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[]
+					 * (besides, doing so would confuse _bt_search, since it
+					 * isn't directly aware of NEXTPRIOR sentinel values)
+					 */
+					break;
+				}
+
 				/*
 				 * Adjust strat_total, and quit if we have stored a > or <
 				 * key.
 				 */
-				strat = chosen->sk_strategy;
-				if (strat != BTEqualStrategyNumber)
+				if (chosen->sk_strategy != BTEqualStrategyNumber)
 				{
-					strat_total = strat;
-					if (strat == BTGreaterStrategyNumber ||
-						strat == BTLessStrategyNumber)
+					strat_total = chosen->sk_strategy;
+					if (chosen->sk_strategy == BTGreaterStrategyNumber ||
+						chosen->sk_strategy == BTLessStrategyNumber)
 						break;
 				}
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index d1423bd85..5a7b1ace4 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND c > 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c' (and so 'c' will still have an inequality scan key,
+ * required in only one direction -- 'c' won't be output as a "range" skip
+ * key/array).
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using opclass skip
+ * support routines.  This can be used to quantify the peformance benefit that
+ * comes from having dedicated skip support, with a given test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup set to valid routine? */
+	Oid			eq_op;			/* InvalidOid means don't skip */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -64,15 +92,38 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno,
+							BTSkipPreproc *skipatts);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
-										   Datum arrdatum, ScanKey cur);
+										   Datum arrdatum, bool arrnull,
+										   ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -258,11 +309,19 @@ _bt_freestack(BTStack stack)
  * preprocessing steps are complete.  This will convert the scan key offset
  * references into references to the scan's so->keyData[] output scan keys.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by later preprocessing on output.
+ * _bt_decide_skipatts decides which attributes receive skip arrays.
+ *
  * Caller must pass *numberOfKeys to give us a way to change the number of
  * input scan keys (our output is caller's input).  The returned array can be
  * smaller than scan->keyData[] when we eliminated a redundant array scan key
- * (redundant with some other array scan key, for the same attribute).  Caller
- * uses this to allocate so->keyData[] for the current btrescan.
+ * (redundant with some other array scan key, for the same attribute).  It can
+ * also be larger when we added a skip array/skip scan key.  Caller uses this
+ * to allocate so->keyData[] for the current btrescan.
  *
  * Note: the reason we need to return a temp scan key array, rather than just
  * scribbling on scan->keyData, is that callers are permitted to call btrescan
@@ -275,8 +334,11 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 	Relation	rel = scan->indexRelation;
 	int			numArrayKeyData = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
+				numSkipArrayKeys,
 				output_ikey = 0;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -286,7 +348,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 
 	Assert(scan->numberOfKeys);
 
-	/* Quick check to see if there are any array keys */
+	/*
+	 * Quick check to see if there are any array keys, or any missing keys we
+	 * can generate a "skip scan" array key for ourselves
+	 */
 	numArrayKeys = 0;
 	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
@@ -304,6 +369,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 		}
 	}
 
+	numSkipArrayKeys = _bt_decide_skipatts(scan, skipatts);
+	if (numSkipArrayKeys)
+	{
+		/* At least one skip array scan key must be added to arrayKeyData[] */
+		numArrayKeys += numSkipArrayKeys;
+		/* output scan key buffer allocation needs space for skip scan keys */
+		numArrayKeyData += numSkipArrayKeys;
+	}
+
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
@@ -330,7 +404,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
 	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/*
+	 * Process each array key, and generate skip arrays as needed.  Also copy
+	 * every scan->keyData[] input scan key (whether it's an array or not)
+	 * into the arrayKeyData array we'll return to our caller (barring any
+	 * array scan keys that we could eliminate early through array merging).
+	 */
 	numArrayKeys = 0;
 	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
@@ -348,8 +427,76 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and scan key where indicated by skipatts */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* won't skip using this attribute */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[output_ikey];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * Temporary testing GUC can disable the use of an opclass's skip
+			 * support routine
+			 */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how the
+			 * consed-up "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[output_ikey], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			output_ikey++;		/* keep this scan key/array */
+		}
+
 		/*
-		 * Copy input scan key into temp arrayKeyData scan key array
+		 * Copy input scan key into temp arrayKeyData scan key array.  (From
+		 * here on, cur points at our copy of the input scan key.)
 		 */
 		cur = &arrayKeyData[output_ikey];
 		*cur = scan->keyData[input_ikey];
@@ -521,6 +668,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
 		numArrayKeys++;
 		output_ikey++;			/* keep this scan key/array */
 	}
@@ -634,7 +785,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -685,7 +837,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -695,6 +847,191 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_decide_skipatts() -- set index attributes requiring skip arrays
+ *
+ * _bt_preprocess_array_keys helper function.  Determines which attributes
+ * will require skip arrays/scan keys.  Also sets up skip support callbacks
+ * for attributes whose input opclass have skip support (opclasses without
+ * skip support will fall back on using next-key sentinel values when
+ * advancing the skip array to its next array element).
+ *
+ * Return value is the total number of scan keys to add as "input" scan keys
+ * for further processing within _bt_preprocess_keys.
+ */
+static int
+_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts)
+{
+	Relation	rel = scan->indexRelation;
+	ScanKey		inputsk;
+	AttrNumber	attno_inputsk = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	inputsk = &scan->keyData[0];
+	for (int i = 0;; inputsk++, i++)
+	{
+		int			prev_numSkipArrayKeys = numSkipArrayKeys;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inputsk
+		 */
+		while (attno_skip < attno_inputsk)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				return prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			numSkipArrayKeys++;
+			attno_skip++;
+		}
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i >= scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 * (either in part or in whole)
+		 */
+		if (attno_inputsk > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inputsk (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inputsk < inputsk->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				numSkipArrayKeys++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				return numSkipArrayKeys;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inputsk = inputsk->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inputsk->sk_strategy == BTEqualStrategyNumber ||
+			(inputsk->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required.)
+		 */
+		if (inputsk->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	return numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatts
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false.
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatts)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatts->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										  BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect input opclasses lacking even an equality
+	 * operator, but they're still supported.  Deal with them gracefully.
+	 */
+	if (!OidIsValid(skipatts->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatts->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+														reverse,
+														&skipatts->sksup);
+
+	/* might not have set up skip support routine, but can skip either way */
+	return true;
+}
+
 /*
  * _bt_setup_array_cmp() -- Set up array comparison functions
  *
@@ -987,17 +1324,15 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
@@ -1010,8 +1345,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.  We have to be sure
+	 * that _bt_compare_array_skey/_bt_binsrch_array_skey use the right proc.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1042,11 +1377,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensured that the scalar scan key could be eliminated as redundant
+		 */
+		eliminated = true;
+	}
+	else
+	{
+		/*
+		 * With a skip array it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when earlier preprocessing wasn't able to eliminate a
+		 * redundant scan key inequality due to a lack of cross-type support.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when it
+ * is redundant with (or contradicted by) a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1098,6 +1487,137 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of skip array scan key when it is "redundant with"
+ * a non-array scalar scan key.  The scalar scan key must be an inequality.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array, allowing its elements to be generated within the limits of a range.
+ * Calling here always renders caller's scalar scan key redundant (the key is
+ * applied when the array advances, but that's just an implementation detail).
+ *
+ * Return value indicates if the array already had a lower/upper bound
+ * (whichever caller's scalar scan key was expected to be).  We return true in
+ * the common case where caller's scan key could be successfully rolled into
+ * the skip array.  We return false when we can't do that due to the presence
+ * of a conflicting inequality.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	/*
+	 * We don't expect to have to deal with NULLs in non-array/non-skip scan
+	 * key.  We expect _bt_preprocess_array_keys to avoid generating a skip
+	 * array for an index attribute with an IS NULL input scan key.  (It will
+	 * still do so in the presence of IS NOT NULL input scan keys, but
+	 * _bt_compare_scankey_args is expected to handle those for us.)
+	 */
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(arraysk->sk_flags & SK_SEARCHARRAY);
+	Assert(arraysk->sk_strategy == BTEqualStrategyNumber);
+	Assert(array->num_elems == -1);
+
+	/* Scalar scan key must be a B-Tree inequality, which are always strict */
+	Assert(!(skey->sk_flags & SK_ISNULL));
+	Assert(skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * A skip array scan key always uses the underlying index attribute's
+	 * input opclass, but it's possible that caller's scalar scan key uses a
+	 * cross-type operator.  In cross-type scenarios, skey.sk_argument doesn't
+	 * use the same type as later array elements (which are all just copies of
+	 * datums taken from index tuples, possibly modified by skip support).
+	 *
+	 * We represent the lowest (and highest) possible value in the array using
+	 * the sentinel value -inf (+inf for high_compare).  The only exceptions
+	 * apply when the opclass has skip support: there we can use a copy of the
+	 * skip support routine's low_elem/high_elem instead -- though only when
+	 * there is no corresponding low_compare/high_compare inequality.
+	 *
+	 * _bt_first understands that -inf/+inf indicate that it should use the
+	 * low_compare/high_compare inequality for initial positioning purposes
+	 * when it sees either value (unless there is no corresponding inequality,
+	 * in which case the values are literally interpreted as -inf or +inf).
+	 * _bt_first can therefore vary in whether it uses a cross-type operator,
+	 * or an input-opclass-only operator (it can vary across primitive scans
+	 * for the same index attribute/skip array).
+	 *
+	 * _bt_scankey_decrement/_bt_scankey_increment both make sure that each
+	 * newly generated element is constrained by low_compare/high_compare.
+	 * This must happen without skey.sk_argument ever being treated as a true
+	 * array element (that wouldn't always work because array elements are
+	 * only ever supposed to use the opclass input type).
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare */
+
+				/* replace old high_compare with new one */
+			}
+			else
+				array->high_compare = palloc(sizeof(ScanKeyData));
+
+			memcpy(array->high_compare, skey, sizeof(ScanKeyData));
+			array->order_high = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare */
+
+				/* replace old low_compare with new one */
+			}
+			else
+				array->low_compare = palloc(sizeof(ScanKeyData));
+
+			memcpy(array->low_compare, skey, sizeof(ScanKeyData));
+			array->order_low = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
@@ -1140,7 +1660,8 @@ _bt_compare_array_elements(const void *a, const void *b, void *arg)
 static inline int32
 _bt_compare_array_skey(FmgrInfo *orderproc,
 					   Datum tupdatum, bool tupnull,
-					   Datum arrdatum, ScanKey cur)
+					   Datum arrdatum, bool arrnull,
+					   ScanKey cur)
 {
 	int32		result = 0;
 
@@ -1148,14 +1669,14 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 
 	if (tupnull)				/* NULL tupdatum */
 	{
-		if (cur->sk_flags & SK_ISNULL)
+		if (arrnull)
 			result = 0;			/* NULL "=" NULL */
 		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = -1;		/* NULL "<" NOT_NULL */
 		else
 			result = 1;			/* NULL ">" NOT_NULL */
 	}
-	else if (cur->sk_flags & SK_ISNULL) /* NOT_NULL tupdatum, NULL arrdatum */
+	else if (arrnull)			/* NOT_NULL tupdatum, NULL arrdatum */
 	{
 		if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = 1;			/* NOT_NULL ">" NULL */
@@ -1221,6 +1742,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1256,7 +1779,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[low_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result <= 0)
 				{
@@ -1284,7 +1807,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[high_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result >= 0)
 				{
@@ -1311,7 +1834,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 		arrdatum = array->elem_values[mid_elem];
 
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										arrdatum, cur);
+										arrdatum, false, cur);
 
 		if (result == 0)
 		{
@@ -1336,13 +1859,102 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	 */
 	if (low_elem != mid_elem)
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										array->elem_values[low_elem], cur);
+										array->elem_values[low_elem], false,
+										cur);
 
 	*set_elem_result = result;
 
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * This routine doesn't return an index into the array, because the array
+ * doesn't actually have any elements (it generates its array elements
+ * procedurally instead).  Note that this may include a NULL value/an IS NULL
+ * qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1352,29 +1964,496 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
 		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
 		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, curArrayKey,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppoDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Clear possibly-irrelevant flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Lowest (or highest) element is NULL, so set scan key to NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Lowest array element isn't NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Highest array element isn't NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		underflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & SK_BT_NEXTPRIOR));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the NEXTPRIOR flag.  The true prior value can only
+	 * be determined when the scan reads lower sorting tuples.
+	 *
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, _bt_first can find the highest non-NULL.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * low_compare is for an >= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true prior value
+		 * cannot possibly satisfy low_compare.  We can give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(&array->order_low,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, false,
+								   skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true prior value */
+		skey->sk_flags |= SK_BT_NEXTPRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &underflow);
+
+	if (underflow)
+	{
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/*
+			 * Existing sk_argument was already equal to non-NULL low_elem
+			 * provided by opclass skip support routine, but skip array's true
+			 * lowest element is actually NULL.
+			 *
+			 * Decrement sk_argument to NULL.
+			 */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the skip array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		overflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & SK_BT_NEXTPRIOR));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXTPRIOR flag.  The true next value can only be
+	 * determined when the scan reads higher sorting tuples.
+	 *
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, _bt_first can find the lowest non-NULL.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * high_compare is for an <= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true next value cannot
+		 * possibly satisfy high_compare.  We can give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(&array->order_high,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, false,
+								   skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true next value */
+		skey->sk_flags |= SK_BT_NEXTPRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &overflow);
+
+	if (overflow)
+	{
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/*
+			 * Existing sk_argument was already equal to non-NULL high_elem
+			 * provided by opclass skip support routine, but skip array's true
+			 * highest element is actually NULL.
+			 *
+			 * Increment sk_argument to NULL.
+			 */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the skip array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1390,6 +2469,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1399,29 +2479,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1476,6 +2557,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1483,7 +2565,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1495,16 +2576,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1568,6 +2643,8 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 	for (int ikey = sktrig; ikey < so->numberOfKeys; ikey++)
 	{
 		ScanKey		cur = so->keyData + ikey;
+		Datum		sk_argument = cur->sk_argument;
+		bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
 		Datum		tupdatum;
 		bool		tupnull;
 		int32		result;
@@ -1629,9 +2706,66 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_NEGPOSINF))
+		{
+			/* The scankey has a conventional sk_argument/element value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											sk_argument, sk_isnull, cur);
+
+			/*
+			 * When scan key is marked NEXTPRIOR, the current array element is
+			 * "sk_argument + infinitesimal" (or the current array element is
+			 * "sk_argument - infinitesimal", during backwards scans)
+			 */
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXTPRIOR))
+			{
+				/*
+				 * tupdatum is actually still < "sk_argument + infinitesimal"
+				 * (or it's actually still > "sk_argument - infinitesimal")
+				 */
+				return true;
+			}
+		}
+		else
+		{
+			/*
+			 * The scankey searches for the sentinel value -inf/+inf.
+			 *
+			 * Note: -inf could mean "absolute" -inf, or it could represent
+			 * the lowest possible value that still satisfies the array's
+			 * low_compare.  +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * Compare tupdatum against -inf using array's low_compare, if any
+			 * (or compare it against +inf using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is > -inf sk_argument (or < +inf sk_argument).
+				 * It's time for caller to advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1963,18 +3097,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1999,18 +3124,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2028,15 +3144,27 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
+			Datum		sk_argument = cur->sk_argument;
+			bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
+
 			Assert(sktrig_required && required);
 
 			/*
@@ -2050,7 +3178,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 */
 			result = _bt_compare_array_skey(&so->orderProcs[ikey],
 											tupdatum, tupnull,
-											cur->sk_argument, cur);
+											sk_argument, sk_isnull, cur);
 		}
 
 		/*
@@ -2109,11 +3237,65 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  This is often the range -inf through to +inf.
+		 * "Binary searching" a skip array only determines whether tupdatum is
+		 * beyond its range, before its range, or within its range.
+		 *
+		 * Note: conventional arrays cannot use this approach.  They need
+		 * "beyond end of array element" advancement to distinguish between
+		 * the final array element (where incremental advancement rolls over
+		 * to the next most significant array), and some earlier array element
+		 * (where incremental advancement just increments set_elem/cur_elem).
+		 * That distinction doesn't exist when dealing with range skip arrays.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == some particular skip array element.
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2464,6 +3646,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also cons up skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2587,8 +3771,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		inputsk = scan->keyData;
 
 	/*
-	 * Now that we have an estimate of the number of output scan keys,
-	 * allocate space for them
+	 * Now that we have an estimate of the number of output scan keys
+	 * (including any skip array scan keys), allocate space for them
 	 */
 	so->keyData = palloc(sizeof(ScanKeyData) * numberOfKeys);
 
@@ -2724,7 +3908,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
+						Assert(!array || array->num_elems > 0 ||
+							   array->num_elems == -1);
 						xform[j].skey = NULL;
 						xform[j].ikey = -1;
 					}
@@ -2887,7 +4072,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
+					Assert(!array || array->num_elems > 0 ||
+						   array->num_elems == -1);
 
 					/*
 					 * New key is more restrictive, and so replaces old key...
@@ -3029,10 +4215,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3107,6 +4294,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3180,6 +4383,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3743,6 +4947,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index db6ed784a..86a5de8f4 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 8efb4044d..6efa3e353 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -372,6 +372,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index edb09d4e3..e945686c8 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -96,6 +96,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 9c854e0e5..79658f068 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,49 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		*underflow = true;
+		return 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		*overflow = true;
+		return 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 date_finite(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index 8c6fc80c3..91682edd5 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -83,6 +83,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index bf42393be..4c1841a2b 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -192,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -213,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5731,6 +5735,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6789,6 +6879,54 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a
+ * single-column index, or C * 0.75 for multiple columns. (The idea here
+ * is that multiple columns dilute the importance of the first column's
+ * ordering, but don't negate it entirely.  Before 8.0 we divided the
+ * correlation by the number of columns, but that seems too strong.)
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6798,17 +6936,21 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6824,13 +6966,17 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
@@ -6841,13 +6987,81 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6889,7 +7103,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6905,6 +7119,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6920,6 +7166,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -7028,104 +7275,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 5284d23dc..81c3494ea 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -384,6 +387,70 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	*underflow = false;
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	*underflow = true;
+
+	return 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	*overflow = false;
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	*overflow = true;
+
+	return 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 521ec5591..239baa7f3 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1751,6 +1752,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3590,6 +3602,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..9662fb2ba 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -583,6 +583,19 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure via an index <quote>skip scan</quote>.  The
+     APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index e3c1539a1..4c586bc8a 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -809,7 +809,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..433e108b8 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intevening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,10 +511,7 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
+   Multicolumn indexes should be used judiciously.  See
    <xref linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
@@ -669,9 +669,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 22d8ad1aa..63f03f3a7 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1056,7 +1063,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1069,7 +1077,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1082,7 +1091,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..8b6b775c1 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index cf6eac573..f7b3ecef4 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1938,7 +1938,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -1958,7 +1958,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 31fb7d142..8c2a939b0 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4370,24 +4370,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -7482,19 +7483,23 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                        QUERY PLAN                         
------------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Nested Loop
    ->  Hash Join
          Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-         ->  Seq Scan on fkest f2
-               Filter: (x100 = 2)
+         ->  Bitmap Heap Scan on fkest f2
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
          ->  Hash
-               ->  Seq Scan on fkest f1
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f1
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
          Index Cond: (x = f1.x)
-(10 rows)
+(14 rows)
 
 alter table fkest add constraint fk
   foreign key (x, x10b, x100) references fkest (x, x10, x100);
@@ -7503,20 +7508,24 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                     QUERY PLAN                      
------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Hash Join
    Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
    ->  Hash Join
          Hash Cond: (f3.x = f2.x)
          ->  Seq Scan on fkest f3
          ->  Hash
-               ->  Seq Scan on fkest f2
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f2
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Hash
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-(11 rows)
+         ->  Bitmap Heap Scan on fkest f1
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
+(15 rows)
 
 rollback;
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 6aeb7cb96..f4c696ca5 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5193,9 +5193,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 0456d48c9..39aa1f89e 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1461,18 +1461,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..4246afefd 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index e296891ca..1d269dc30 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -766,7 +766,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -776,7 +776,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index df3f336be..18fec46e7 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2662,6 +2663,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

#19Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#18)
3 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Hi,

I started looking at this patch today. The first thing I usually do for
new patches is a stress test, so I did a simple script that generates
random table and runs a random query with IN() clause with various
configs (parallel query, index-only scans, ...). And it got stuck on a
parallel query pretty quick.

I've seen a bunch of those cases, so it's not a particularly unlikely
issue. The backtraces look pretty much the same in all cases - the
processes are stuck either waiting on the conditional variable in
_bt_parallel_seize, or trying to send data in shm_mq_send_bytes.

Attached is the script I use for stress testing (pretty dumb, just a
bunch of loops generating tables + queries), and backtraces for two
lockups (one is EXPLAIN ANALYZE, but otherwise exactly the same).

I haven't investigated why this is happening, but I wonder if this might
be similar to the parallel hashjoin issues, with trying to send data,
but the receiver being unable to proceed and effectively working on the
sender. But that's just a wild guess.

regards

--
Tomas Vondra

Attachments:

lockup2.logtext/x-log; charset=UTF-8; name=lockup2.logDownload
lockup.logtext/x-log; charset=UTF-8; name=lockup.logDownload
run.shapplication/x-shellscript; name=run.shDownload
In reply to: Tomas Vondra (#19)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Sat, Sep 7, 2024 at 11:27 AM Tomas Vondra <tomas@vondra.me> wrote:

I started looking at this patch today.

Thanks for taking a look!

The first thing I usually do for
new patches is a stress test, so I did a simple script that generates
random table and runs a random query with IN() clause with various
configs (parallel query, index-only scans, ...). And it got stuck on a
parallel query pretty quick.

I can reproduce this locally, without too much difficulty.
Unfortunately, this is a bug on master/Postgres 17. Some kind of issue
in my commit 5bf748b8.

The timing of this is slightly unfortunate. There's only a few weeks
until the release of 17, plus I have to travel for work over the next
week. I won't be back until the 16th, and will have limited
availability between then and now. I think that I'll have ample time
to debug and fix the issue ahead of the release of 17, though.

Looks like the problem is a parallel index scan with SAOP array keys
can find itself in a state where every parallel worker waits for the
leader to finish off a scheduled primitive index scan, while the
leader itself waits for the scan's tuple queue to return more tuples.
Obviously, the query will effectively go to sleep indefinitely when
that happens (unless and until the DBA cancels the query). This is
only possible with just the right/wrong combination of array keys and
index cardinality.

I cannot recreate the problem with parallel_leader_participation=off,
which strongly suggests that leader participation is a factor. I'll
find time to study this in detail as soon as I can.

Further background: I was always aware of the leader's tendency to go
away forever shortly after the scan begins. That was supposed to be
safe, since we account for it by serializing the scan's current array
keys in shared memory, at the point a primitive index scan is
scheduled -- any backend should be able to pick up where any other
backend left off, no matter how primitive scans are scheduled. That
now doesn't seem to be completely robust, likely due to restrictions
on when and how other backends can pick up the scheduled work from
within _bt_first, at the point that it calls _bt_parallel_seize.

In short, one or two details of how backends call _bt_parallel_seize
to pick up BTPARALLEL_NEED_PRIMSCAN work likely need to be rethought.

--
Peter Geoghegan

#21Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Peter Geoghegan (#20)
2 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Mon, 9 Sept 2024 at 21:55, Peter Geoghegan <pg@bowt.ie> wrote:

On Sat, Sep 7, 2024 at 11:27 AM Tomas Vondra <tomas@vondra.me> wrote:

I started looking at this patch today.

Thanks for taking a look!

The first thing I usually do for
new patches is a stress test, so I did a simple script that generates
random table and runs a random query with IN() clause with various
configs (parallel query, index-only scans, ...). And it got stuck on a
parallel query pretty quick.

I can reproduce this locally, without too much difficulty.
Unfortunately, this is a bug on master/Postgres 17. Some kind of issue
in my commit 5bf748b8.

[...]

In short, one or two details of how backends call _bt_parallel_seize
to pick up BTPARALLEL_NEED_PRIMSCAN work likely need to be rethought.

Thanks to Peter for the description, that helped me debug the issue. I
think I found a fix for the issue: regression tests for 811af978
consistently got stuck on my macbook before the attached patch 0001,
after applying that this patch they completed just fine.

The issue to me seems to be the following:

Only _bt_first can start a new primitive scan, so _bt_parallel_seize
only assigns a new primscan if the process is indeed in _bt_first (as
provided with _b_p_s(first=true)). All other backends that hit a
NEED_PRIMSCAN state will currently pause until a backend in _bt_first
does the next primitive scan.

A backend that hasn't requested the next primitive scan will likely
hit _bt_parallel_seize from code other than _bt_first, thus pausing.
If this is the leader process, it'll stop consuming tuples from
follower processes.

If the follower process finds a new primary scan is required after
finishing reading results from a page, it will first request a new
primitive scan, and only then start producing the tuples.

As such, we can have a follower process that just finished reading a
page, had issued a new primitive scan, and now tries to send tuples to
its primary process before getting back to _bt_first, but the its
primary process won't acknowledge any tuples because it's waiting for
that process to start the next primitive scan - now we're deadlocked.

---

The fix in 0001 is relatively simple: we stop backends from waiting
for a concurrent backend to resolve the NEED_PRIMSCAN condition, and
instead move our local state machine so that we'll hit _bt_first
ourselves, so that we may be able to start the next primitive scan.
Also attached is 0002, which adds tracking of responsible backends to
parallel btree scans, thus allowing us to assert we're never waiting
for our own process to move the state forward. I found this patch
helpful while working on solving this issue, even if it wouldn't have
found the bug as reported.

Kind regards,

Matthias van de Meent
Neon (https://neon.tech)

Attachments:

v1-0001-Fix-stuck-parallel-btree-scans.patchapplication/octet-stream; name=v1-0001-Fix-stuck-parallel-btree-scans.patchDownload
From db0f4800d3bae875b5b1b262249c12738a243bf7 Mon Sep 17 00:00:00 2001
From: Matthias van de Meent <boekewurm+postgres@gmail.com>
Date: Thu, 12 Sep 2024 15:23:20 +0100
Subject: [PATCH v1 1/2] Fix stuck parallel btree scans

Before, a backend that called _bt_parallel_seize was not always
guaranteed to be able to move forward on a state where more work
was expected from parallel backends, and handled NEED_PRIMSCAN as
a semi-ADVANCING state. This caused issues when the leader process
was waiting for the state to advance and concurrent backends were
waiting for the leader to consume the buffered tuples they still
had after updating the state to NEED_PRIMSCAN.

This is fixed by treating _bt_parallel_seize()'s status output as
the status of a currently active primitive scan.  If _seize is
called from outside _bt_first, and the scan state is NEED_PRIMSCAN,
then we'll end our current primitive scan and set the scan up for
a new primitive scan, eventually hitting _bt_first's call to
_seize.
---
 src/backend/access/nbtree/nbtree.c | 16 +++++++++++++---
 1 file changed, 13 insertions(+), 3 deletions(-)

diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 6d090f8739..2b553d1161 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -584,7 +584,8 @@ btparallelrescan(IndexScanDesc scan)
  *		or _bt_parallel_done().
  *
  * The return value is true if we successfully seized the scan and false
- * if we did not.  The latter case occurs if no pages remain.
+ * if we did not.  The latter case occurs if no pages remain in this primitive
+ * index scan.
  *
  * If the return value is true, *pageno returns the next or current page
  * of the scan (depending on the scan direction).  An invalid block number
@@ -653,8 +654,10 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			Assert(so->numArrayKeys);
 
 			/*
-			 * If we can start another primitive scan right away, do so.
-			 * Otherwise just wait.
+			 * If we're called from _bt_first and thus are set up to start a
+			 * primitive scan, do so.  If not, we stop this current primitive
+			 * scan by returning false, which sets us up for the call to
+			 * _bt_first which can then try to seize this scan again.
 			 */
 			if (first)
 			{
@@ -672,6 +675,13 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 				*pageno = InvalidBlockNumber;
 				exit_loop = true;
 			}
+			else
+			{
+				so->needPrimScan = true;
+				so->scanBehind = false;
+				*pageno = InvalidBlockNumber;
+				status = false;
+			}
 		}
 		else if (btscan->btps_pageStatus != BTPARALLEL_ADVANCING)
 		{
-- 
2.46.0

v1-0002-nbtree-add-tracking-of-processing-responsibilitie.patchapplication/octet-stream; name=v1-0002-nbtree-add-tracking-of-processing-responsibilitie.patchDownload
From e04836e0e1c822f586778d489c8b7ea6708feec5 Mon Sep 17 00:00:00 2001
From: Matthias van de Meent <boekewurm+postgres@gmail.com>
Date: Thu, 12 Sep 2024 15:27:02 +0100
Subject: [PATCH v1 2/2] nbtree: add tracking of processing responsibilities in
 BTPSD

By tracking which proc is responsible for moving the state forward, we can
make assertions about the scan moving forward, and also assign blame to a
specific backend when we still get stuck.
---
 src/backend/access/nbtree/nbtree.c | 36 ++++++++++++++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 2b553d1161..0324860451 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -72,6 +72,10 @@ typedef struct BTParallelScanDescData
 									 * possible states of parallel scan. */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
+#ifdef USE_ASSERT_CHECKING
+	ProcNumber	btps_procnumber;	/* procnumber of backend currently
+									 * advancing the scan */
+#endif
 
 	/*
 	 * btps_arrElems is used when scans need to schedule another primitive
@@ -550,6 +554,9 @@ btinitparallelscan(void *target)
 	SpinLockInit(&bt_target->btps_mutex);
 	bt_target->btps_scanPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+#if USE_ASSERT_CHECKING
+	bt_target->btps_procnumber = INVALID_PROC_NUMBER;
+#endif
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -575,6 +582,9 @@ btparallelrescan(IndexScanDesc scan)
 	SpinLockAcquire(&btscan->btps_mutex);
 	btscan->btps_scanPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+#if USE_ASSERT_CHECKING
+	btscan->btps_procnumber = INVALID_PROC_NUMBER;
+#endif
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -642,6 +652,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 
 	while (1)
 	{
+#ifdef USE_ASSERT_CHECKING
+		ProcNumber	waitingFor;
+#endif
 		SpinLockAcquire(&btscan->btps_mutex);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
@@ -674,6 +687,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 				so->scanBehind = false;
 				*pageno = InvalidBlockNumber;
 				exit_loop = true;
+#ifdef USE_ASSERT_CHECKING
+				btscan->btps_procnumber = MyProcNumber;
+#endif
 			}
 			else
 			{
@@ -690,12 +706,20 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			 * of advancing it to a new page!
 			 */
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
+#ifdef USE_ASSERT_CHECKING
+			btscan->btps_procnumber = MyProcNumber;
+#endif
 			*pageno = btscan->btps_scanPage;
 			exit_loop = true;
 		}
+#ifdef USE_ASSERT_CHECKING
+		waitingFor = btscan->btps_procnumber;
+#endif
 		SpinLockRelease(&btscan->btps_mutex);
 		if (exit_loop || !status)
 			break;
+
+		Assert(waitingFor != MyProcNumber && waitingFor != INVALID_PROC_NUMBER);
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
 	}
 	ConditionVariableCancelSleep();
@@ -726,6 +750,10 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber scan_page)
 	SpinLockAcquire(&btscan->btps_mutex);
 	btscan->btps_scanPage = scan_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
+#if USE_ASSERT_CHECKING
+	Assert(btscan->btps_procnumber == MyProcNumber);
+	btscan->btps_procnumber = INVALID_PROC_NUMBER;
+#endif
 	SpinLockRelease(&btscan->btps_mutex);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
@@ -758,6 +786,11 @@ _bt_parallel_done(IndexScanDesc scan)
 	SpinLockAcquire(&btscan->btps_mutex);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
+#if USE_ASSERT_CHECKING
+		Assert(btscan->btps_procnumber == MyProcNumber);
+		btscan->btps_procnumber = INVALID_PROC_NUMBER;
+#endif
+
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
@@ -792,6 +825,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 	if (btscan->btps_scanPage == prev_scan_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
+#ifdef USE_ASSERT_CHECKING
+		Assert(btscan->btps_procnumber == INVALID_PROC_NUMBER);
+#endif
 		btscan->btps_scanPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
 
-- 
2.46.0

#22Tomas Vondra
tomas@vondra.me
In reply to: Matthias van de Meent (#21)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 9/12/24 16:49, Matthias van de Meent wrote:

On Mon, 9 Sept 2024 at 21:55, Peter Geoghegan <pg@bowt.ie> wrote:

...

The fix in 0001 is relatively simple: we stop backends from waiting
for a concurrent backend to resolve the NEED_PRIMSCAN condition, and
instead move our local state machine so that we'll hit _bt_first
ourselves, so that we may be able to start the next primitive scan.
Also attached is 0002, which adds tracking of responsible backends to
parallel btree scans, thus allowing us to assert we're never waiting
for our own process to move the state forward. I found this patch
helpful while working on solving this issue, even if it wouldn't have
found the bug as reported.

No opinion on the analysis / coding, but per my testing the fix indeed
addresses the issue. The script reliably got stuck within a minute, now
it's running for ~1h just fine. It also checks results and that seems
fine too, so that seems fine too.

regards

--
Tomas Vondra

In reply to: Matthias van de Meent (#21)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Thu, Sep 12, 2024 at 10:49 AM Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:

Thanks to Peter for the description, that helped me debug the issue. I
think I found a fix for the issue: regression tests for 811af978
consistently got stuck on my macbook before the attached patch 0001,
after applying that this patch they completed just fine.

Thanks for taking a look at it.

The fix in 0001 is relatively simple: we stop backends from waiting
for a concurrent backend to resolve the NEED_PRIMSCAN condition, and
instead move our local state machine so that we'll hit _bt_first
ourselves, so that we may be able to start the next primitive scan.

I agree with your approach, but I'm concerned about it causing
confusion inside _bt_parallel_done. And so I attach a v2 revision of
your bug fix. v2 adds a check that nails that down, too. I'm not 100%
sure if the change to _bt_parallel_done becomes strictly necessary, to
make the basic fix robust, but it's a good idea either way. In fact, it
seemed like a good idea even before this bug came to light: it was
already clear that this was strictly necessary for the skip scan
patch. And for reasons that really have nothing to do with the
requirements for skip scan (it's related to how we call
_bt_parallel_done without much care in code paths from the original
parallel index scan commit).

More details on changes in v2 that didn't appear in Matthias' v1:

v2 makes _bt_parallel_done do nothing at all when the backend-local
so->needPrimScan flag is set (regardless of whether it has been set by
_bt_parallel_seize or by _bt_advance_array_keys). This is a bit like
the approach taken before the Postgres 17 work went in:
_bt_parallel_done used to only permit the shared btps_pageStatus state
to become BTPARALLEL_DONE when it found that "so->arrayKeyCount >=
btscan->btps_arrayKeyCount" (else the call was a no-op). With this
extra hardening, _bt_parallel_done will only permit setting BTPARALLEL_DONE when
"!so->needPrimScan". Same idea, more or less.

v2 also changes comments in _bt_parallel_seize. The comment tweaks
suggest that the new "if (!first && status ==
BTPARALLEL_NEED_PRIMSCAN) return false" path is similar to the
existing master branch "if (!first && so->needPrimScan) return false"
precheck logic on master (the precheck that takes place before
examining any state in shared memory). The new path can be thought of
as dealing with cases where the backend-local so->needPrimScan flag
must have been stale back when it was prechecked -- it's essentially the same
logic, though unlike the precheck it works against the authoritative
shared memory state.

My current plan is to commit something like this in the next day or two.

--
Peter Geoghegan

Attachments:

v2-0001-Fix-stuck-parallel-btree-scans.patchapplication/x-patch; name=v2-0001-Fix-stuck-parallel-btree-scans.patchDownload
From 11282515bae8090b30663814c5f91db00488508d Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 16 Sep 2024 14:28:57 -0400
Subject: [PATCH v2] Fix stuck parallel btree scans

Before, a backend that called _bt_parallel_seize was not always
guaranteed to be able to move forward on a state where more work
was expected from parallel backends, and handled NEED_PRIMSCAN as
a semi-ADVANCING state. This caused issues when the leader process
was waiting for the state to advance and concurrent backends were
waiting for the leader to consume the buffered tuples they still
had after updating the state to NEED_PRIMSCAN.

This is fixed by treating _bt_parallel_seize()'s status output as
the status of a currently active primitive scan.  If _seize is
called from outside _bt_first, and the scan state is NEED_PRIMSCAN,
then we'll end our current primitive scan and set the scan up for
a new primitive scan, eventually hitting _bt_first's call to
_seize.

Oversight in commit 5bf748b8, which enhanced nbtree ScalarArrayOp
execution.

Author: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reported-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-WzmMGaPa32u9x_FvEbPTUkP5e95i=QxR8054nvCRydP-sw@mail.gmail.com
Backpatch: 17-, where nbtree SAOP execution was enhanced.
---
 src/backend/access/nbtree/nbtree.c | 53 +++++++++++++++++++-----------
 1 file changed, 33 insertions(+), 20 deletions(-)

diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 686a3206f..456a04995 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -585,7 +585,10 @@ btparallelrescan(IndexScanDesc scan)
  *		or _bt_parallel_done().
  *
  * The return value is true if we successfully seized the scan and false
- * if we did not.  The latter case occurs if no pages remain.
+ * if we did not.  The latter case occurs when no pages remain, or when
+ * another primitive index scan is scheduled that caller's backend cannot
+ * start just yet (only backends that call from _bt_first are capable of
+ * starting primitive index scans, which they indicate by passing first=true).
  *
  * If the return value is true, *pageno returns the next or current page
  * of the scan (depending on the scan direction).  An invalid block number
@@ -596,10 +599,6 @@ btparallelrescan(IndexScanDesc scan)
  * scan will return false.
  *
  * Callers should ignore the value of pageno if the return value is false.
- *
- * Callers that are in a position to start a new primitive index scan must
- * pass first=true (all other callers pass first=false).  We just return false
- * for first=false callers that require another primitive index scan.
  */
 bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
@@ -616,13 +615,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 	{
 		/*
 		 * Initialize array related state when called from _bt_first, assuming
-		 * that this will either be the first primitive index scan for the
-		 * scan, or a previous explicitly scheduled primitive scan.
-		 *
-		 * Note: so->needPrimScan is only set when a scheduled primitive index
-		 * scan is set to be performed in caller's worker process.  It should
-		 * not be set here by us for the first primitive scan, nor should we
-		 * ever set it for a parallel scan that has no array keys.
+		 * that this will be the first primitive index scan for the scan
 		 */
 		so->needPrimScan = false;
 		so->scanBehind = false;
@@ -630,8 +623,8 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 	else
 	{
 		/*
-		 * Don't attempt to seize the scan when backend requires another
-		 * primitive index scan unless we're in a position to start it now
+		 * Don't attempt to seize the scan when it requires another primitive
+		 * index scan, since caller's backend cannot start it right now
 		 */
 		if (so->needPrimScan)
 			return false;
@@ -653,12 +646,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 		{
 			Assert(so->numArrayKeys);
 
-			/*
-			 * If we can start another primitive scan right away, do so.
-			 * Otherwise just wait.
-			 */
 			if (first)
 			{
+				/* Can start another primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 				for (int i = 0; i < so->numArrayKeys; i++)
 				{
@@ -668,11 +658,25 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 					array->cur_elem = btscan->btps_arrElems[i];
 					skey->sk_argument = array->elem_values[array->cur_elem];
 				}
-				so->needPrimScan = true;
-				so->scanBehind = false;
 				*pageno = InvalidBlockNumber;
 				exit_loop = true;
 			}
+			else
+			{
+				/*
+				 * Don't attempt to seize the scan when it requires another
+				 * primitive index scan, since caller's backend cannot start
+				 * it right now
+				 */
+				status = false;
+			}
+
+			/*
+			 * Either way, update backend local state to indicate that a
+			 * pending primitive scan is required
+			 */
+			so->needPrimScan = true;
+			so->scanBehind = false;
 		}
 		else if (btscan->btps_pageStatus != BTPARALLEL_ADVANCING)
 		{
@@ -731,6 +735,7 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber scan_page)
 void
 _bt_parallel_done(IndexScanDesc scan)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
 	bool		status_changed = false;
@@ -739,6 +744,13 @@ _bt_parallel_done(IndexScanDesc scan)
 	if (parallel_scan == NULL)
 		return;
 
+	/*
+	 * Should not mark parallel scan done when there's still a pending
+	 * primitive index scan
+	 */
+	if (so->needPrimScan)
+		return;
+
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
@@ -747,6 +759,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * already
 	 */
 	SpinLockAcquire(&btscan->btps_mutex);
+	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
-- 
2.45.2

#24Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#18)
3 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Hi,

I've been looking at this patch over the couple last days, mostly doing
some stress testing / benchmarking (hence the earlier report) and basic
review. I do have some initial review comments, and the testing produced
some interesting regressions (not sure if those are the cases where
skipscan can't really help, that Peter mentioned he needs to look into).

review
------

First, the review comments - nothing particularly serious, mostly just
cosmetic stuff:

1) v6-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patch

- I find the places that increment "nsearches" a bit random. Each AM
does it in entirely different place (at least it seems like that to me).
Is there a way make this a bit more consistent?

- I find this comment rather unhelpful:

uint64 btps_nsearches; /* instrumentation */

Instrumentation what? What's the counter for?

- I see _bt_first moved the pgstat_count_index_scan, but doesn't that
mean we skip it if the earlier code does "goto readcomplete"? Shouldn't
that still count as an index scan?

- show_indexscan_nsearches does this:

if (scanDesc && scanDesc->nsearches > 0)
ExplainPropertyUInteger("Index Searches", NULL,
scanDesc->nsearches, es);

But shouldn't it divide the count by nloops, similar to (for example)
show_instrumentation_count?

2) v6-0002-Normalize-nbtree-truncated-high-key-array-behavio.patch

- Admittedly very subjective, but I find the "oppoDirCheck" abbreviation
rather weird, I'd just call it "oppositeDirCheck".

3) v6-0003-Refactor-handling-of-nbtree-array-redundancies.patch

- nothing

4) v6-0004-Add-skip-scan-to-nbtree.patch

- indices.sgml seems to hahve typo "Intevening" -> "Intervening"

- It doesn't seem like a good idea to remove the paragraph about
multicolumn indexes and replace it with just:

Multicolumn indexes should be used judiciously.

I mean, what does judiciously even mean? what should the user consider
to be judicious? Seems rather unclear to me. Admittedly, the old text
was not much helpful, but at least it gave some advice.

But maybe more importantly, doesn't skipscan apply only to a rather
limited subset of data types (that support increment/decrement)? Doesn't
the new wording mostly ignore that, implying skipscan applies to all
btree indexes? I don't think it mentions datatypes anywhere, but there
are many indexes on data types like text, UUID and so on.

- Very subjective nitpicking, but I find it a bit strange when a comment
about a block is nested in the block, like in _bt_first() for the
array->null_elem check.

- assignProcTypes() claims providing skipscan for cross-type scenarios
doesn't make sense. Why is that? I'm not saying the claim is wrong, but
it's not clear to me why would that be the case.

costing
-------

Peter asked me to look at the costing, and I think it looks generally
sensible. We don't really have a lot of information to base the costing
on in the first place - the whole point of skipscan is about multicolumn
indexes, but none of the existing extended statistic seems very useful.
We'd need some cross-column correlation info, or something like that.

It's an interesting question - if we could collect some new statistics
for multicolumn indexes (say, by having a way to collect AM-specific
stats), what would we collect for skipscan?

There's one thing that I don't quite understand, and that's how
btcost_correlation() adjusts correlation for multicolumn indexes:

if (index->nkeycolumns > 1)
indexCorrelation = varCorrelation * 0.75;

That seems fine for a two-column index, I guess. But shouldn't it
compound for indexes with more keys? I mean, 0.75 * 0.75 for third
column, etc? I don't think btcostestimate() does that, it just remembers
whatever btcost_correlation() returns.

Anyway, the overall costing approach seems sensible, I think. It assumes
things we assume in general (columns/keys are considered independent),
which may be problematic, but this is the best we can do.

The only alternative approach I can think of is not to adjust the
costing for the index scan at all, and only use this to enable (or not
enable) the skipscan internally. That would mean the overall plan
remains the same, and maybe sometimes we would think an index scan would
be too expensive and use something else. Not great, but it doesn't have
the risk of regressions - IIUC we can disable the skipscan at runtime,
if we realize it's not really helpful.

If we're concerned about regressions, I think this would be the way to
deal with them. Or at least it's the best idea I have.

testing
-------

As usual, I wrote a bash script to do a bit of stress testing. It
generates tables with random data, and then runs random queries with
random predicates on them, while mutating a couple parameters (like
number of workers) to trigger different plans. It does that on 16,
master and with the skipscan patch (with the fix for parallel scans).

I've uploaded the script and results from the last run here:

https://github.com/tvondra/pg-skip-scan-tests

There's the "run-mdam.sh" script that generates tables/queries, runs
them, collects all kinds of info about the query, and produces files
with explain plans, CSV with timings, etc.

Not all of the queries end up using index scans - depending on the
predicates, etc. it might have to use seqscan. Or maybe it only uses
index scan because it's forced to by the enable_* options, etc.

Anyway, I ran a couple thousand such queries, and I haven't found any
incorrect results (the script compares that between versions too). So
that's good ;-)

But my main goal was to see how this affects performance. The tables
were pretty small (just 1M rows, maybe ~80MB), but with restarts and
dropping caches, large enough to test this.

And generally the results seem good. You can either inspect the CSV with
raw results (look at the script to undestand what the fields are), or
check the attached PDF with a pivot table summarizing them.

As usual, there's a heatmap on the right side, comparing the results for
different versions (first "master/16" and then "skipscan/master"). Green
means "speedup/good" and red meand "regression/bad".

Most of the places are "white" (no change) or not very far from it, or
perhaps "green". But there's also a bunch of red results, which means
regression (FWIW the PDF is filtered only to queries that would actually
use the executed plans without the GUCs).

Some of the red placees are for very short queries - just a couple ms,
which means it can easily be random noise, or something like that. But a
couple queries are much longer, and might deserve some investigation.
The easiest way is to look at the "QID" column in the row, which
identifies the query in the "query" CSV. Then look into the results CSV
for IDs of the runs (in the first "SEQ" column), and find the details in
the "analyze" log, which has all the plans etc.

Alternatively, use the .ods in the git repository, which allows drill
down to results (from the pivot tables).

For example, one of the slowed down queries is query 702 (top of page 8
in the PDF). The query is pretty simple:

explain (analyze, timing off, buffers off)
select id1,id2 from t_1000000_1000_1_2
where NOT (id1 in (:list)) AND (id2 = :value);

and it was executed on a table with random data in two columns, each
with 1000 distinct values. This is perfectly random data, so a great
match for the assumptions in costing etc.

But with uncached data, this runs in ~50 ms on master, but takes almost
200 ms with skipscan (these timings are from my laptop, but similar to
the results).

-- master
Index Only Scan using t_1000000_1000_1_2_id1_id2_idx on
t_1000000_1000_1_2 (cost=0.96..20003.96 rows=1719 width=16)
(actual rows=811 loops=1)
Index Cond: (id2 = 997)
Filter: (id1 <> ALL ('{983,...,640}'::bigint[]))
Rows Removed by Filter: 163
Heap Fetches: 0
Planning Time: 7.596 ms
Execution Time: 28.851 ms
(7 rows)

-- with skipscan
Index Only Scan using t_1000000_1000_1_2_id1_id2_idx on
t_1000000_1000_1_2 (cost=0.96..983.26 rows=1719 width=16)
(actual rows=811 loops=1)
Index Cond: (id2 = 997)
Index Searches: 1007
Filter: (id1 <> ALL ('{983,...,640}'::bigint[]))
Rows Removed by Filter: 163
Heap Fetches: 0
Planning Time: 3.730 ms
Execution Time: 238.554 ms
(8 rows)

I haven't looked into why this is happening, but this seems like a
pretty good match for skipscan (on the first column). And for the
costing too - it's perfectly random data, no correllation, etc.

regards

--
Tomas Vondra

Attachments:

optimal.pdfapplication/pdf; name=optimal.pdfDownload
%PDF-1.7
%����
4 0 obj
<< /Length 5 0 R
   /Filter /FlateDecode
>>
stream
x��}K�%��������\����X0,��e�EC�ruu�5��Ru����A2&�<�j���U�$O|d0�������������%u�����^d���?�~�^�~���_��R�*U�?+��)�`o^� ����������&��M������������j�����}5�/?�����?����R����N��n�����������o.���+�~��E��H�K���9�2��01��F���>�>����^�H�i3*x�|��:m�'%�U4���+'��.�7��RH����dE�QKW�����V��6���G��(|d�w�)����LF2#�r���Q��	&0	m�u�9
�w���	|��F���|�[ �wR=5��J��P����%�2���W�)�E}�B�l\p#9�r���3�������x����o�x�'`*��'j �j��dcj�PF�R�WI��N:�?0)�!��Q�Q� vF6��ud�2,�dDTh����JF�K+�f���}7H�������
����~���M��@�Qd9.���cYS�I�-A��H`����dP��k�~�[��7�����h���V�
:�1������.�x���1����4~ZlW���m��u���g�o�xK,��T���[&�uaLR�pC��y�����vf��\c�k=?�1�;%�Ygt_@�2B;�W�F<pA��TP-�	��5���36t��1�����TO��(�t�R�Zti�-@�d�(�B
�q�6��_M�6��k��Z$��6Z�d��v)b�6f,��b���y� �'��X��(��tm��o�R"�%i!�JR7T��*�8��@�j�j�eJ5��}��g�c��n�]��%1��w�8�>>~hk����<��f�N�z�Lw�ko���_#�������k���X�� �IkQ���ej����!���vn+�;H������c��^��������:�5V���l���r����� g��E�f?7nJ()^&#��v�y�E���;4p"�c������R6J�����~��i��X9l�RD'��f�Se,{G�rZd{���a$��SV#���L$�����y:5���VpSb9=&�.S ��������������
�P�%LWW�vLW8,���u��p���"���Q���j�����#�'��x2x �z��������	��(�_FE��'���@�����c
@
��y��c\���#z��{o"�{�de�6��'�;q��r��5=5������y���3�~ ���D�M�1�eH�Cl����+
�w
�"��	^��DsE�w�8���v��Q����8b~�%�~��������v+�	�&[\K{������ u�el���W���[\_\�8�B)C�E�S�SE�l�Eq��r�NL�Cd���G���\�������������!��R*x��Q�h�]����<�����6��12��cf�0~������D4D�����OI�
�tR��T�N�2>2�E�5%lp�mM�ZJ��r?:�J��v���\����)�������f��uB�@�t�Y:�s�C�1Hk48QE5���#��{��N�o4�����V��k�*lr��8?I6���-�+@�~�
n�66�8��N���w�h���m�e��\O���
�8`���Sa�0��o��c���CJ)��k�
�w��$��Jy8��#��AO��� <3�����6�1�F8�q�������;dg���t��l���Q��>�O�=D��QC�*��2�_��0��_�����T���k�P�����g�|4~�f������������AtA.<��]m���HH��u<��D���H�)0�����
��@������	�WM��
��������wN���m`��i��`dC��V�3T��?0��
K�j��"�E���w4�����R�$U�=4%E���(��.h�K^����}6j�=��=J]v��GF�Sj�\a����z�%�=YC��������;����v>���������@��*�tB��
�����/ �,!�ML���mc������p��7�~[g!���#��)}�l��K����X�H|��.�������7">��G�g�S-A�d�&d�/~o���Z��Iq����L����@�4p4��K�l�&[�P�]����#_F���Q1_Fr#����� 7�E��?G������,%����U�D�$W
��+����lr��c��.���5���H��������H�6����n�� ��`F�E����%�<�wuiG�����]�~��d�.Y�����Z����EM��N�
��tf�	����J6��-�\�|>!�u?\�+nJ���sTIADK�n2�x�$p3!����Q�#����q_?�'�7W |2`nKj���X���V���r�v���|e���[4����m���)�bu��md���9��1�<:)B����DQ����N�}�N����<�p�c}��+��w>i>�����"���L�i �1��t|��l
��-Cd��XoW�K�8����v��x�8i����v���Xlr G8�	�}��.u�#�<��P='Z�mxq�3��8��y�W5O�
`�76��N�?�] u�E�+��I�qR-����'xhJh�m��Q"Z��r%�^����@�dS?�2��qa ~�t}j����E�D�$c�w�S�Y��M�\� W�;��pa�]��)1�9�qW�5�[%��V77	D���q�h�0�'9?��nm�4G���a�&��=���V���t"��h$8�E5��Ag�9w�<s>�"�n&���:>��T1��6#�'d�MM����:��
�;nJ�:�m]�^(Cz���A/n�L��B6��f3�����FM�-�I��<%���%�,�B)��9y������pa��nJ,~UNDG(M��3E�geE6�\0c��=������\�����~���hM�"(Bg/>O�0g��M$|^;LS6y�#d ���[���!�p��U�K�|�$d����
�h��
����-G��i��V?p8p�����#�^4�<d�������=�{�`
�a��"~T��@'t����4~m|��N.���:�����xZ�wr|�8��.\�f�
�._v���1G�������i/?dN<p�n��[�|'s!f'�\h�y�I$AP��u0w��c����w|�{'��<j �A���@�r���E�%V��$�#/���5'	�xj��G;�X ��~��N >;cz>i�A�����M4p]����������l��x�h���e����Hw��J"O��O���^smpOT1]1%��*����0��
j�vp�oJ����B���U=w�`�-A�T�k�V~ei<�;����������*	�)�@���@������@���c��/S�D ��0g$�d��P�L>b4</�)PA�h��[4��-@r��Gxq�~�����k^9�$G�i�#;
: J7&��x6�;'��+|'��#��sZ�X�H:�h�&l�����z,����O��b�NI=*�NkA=p�z��r�R���r�&��`s=�}g�w����1/�\k������]�8��W8&����?
�c���x����J~��g���5:UtS�^$x�#��gnb�����$j'�����d��3B'�d��z`����)��9���h7����v�b�N�2}��m�kq�Hg7
��,[�0%�R8o}�7t	���k��_c��{ib���9>2��6v<�1?��H���V��B)|����O�?{�	�e�9dGs��)��y'�������7B�nn�I��l~�"o3n����Toz��<p��W
���*��b���9e^<��I��q$3x�h����y O�~���5���G;>�A/5qd��lqQ����"�L7���[�������p�����x���M����wq��������u~7���.\/a�>�{Te��k�>b�W�c�6�������,g�r������F2���C�qe����s��
	w1��w�g;j����Qk1���$w���4�*���%�,�s8�����F��i���KZ(G�
K�YxV�d���]����u�9R9�����/0�	.�P���_��JAH�x	M�����v�e���3oL��x��(_�"$��~s9��
��Ti|�ug��+L
�w�����1�EsR���y[�p������k����,~��SXu3p�0�;��;��]�v�U��^��Q������m����I�~��X~���r�oI&z�@N)��#w�6Z7@��J/2��/�p?y���
�������<;�^XKg��7�l���*��B.��k�:��u�YE��������pZ	#�������	
��*��'(�p��X}�{ �wK������?�K�G������^��//���&��}�������vS��[����8/|J�7oEp����=p�m��������E
e6���2�C'l��%�$�%��������i(���f,�g���$���H�'�����Uq<�f�(��[{0�a*�Z��l�:	�F�'[�3yvbh{���(�������.7xu$<�B�/�A�����)�0-��h���X4��j}*��Px��t�*U���O��YO�sL<yji��|��
V�P����p��#�[H�= ��-2���<�l�JE�����h��A$���h�5Ax���h��A;@��=�+��=q
A:�z�-O��U������E�=c�G[l�L`�E{��j)�~n{t�D�c���=�$�����h�uV�8;����"��M��s��}�����U���!�a
�{\2B�a�G�A$3B��-�\:�y���J'��G�AoFl}������[��.o*{<���CA��h�5J�QI�G�^����h����AN�c>��u�
c�G{�w�������pj����&�����$����=]�������k����m�s.R�~��'�^�N����l*��[���
=���h��sZw���m����(��|<�c���c>����1�������~�G�A$gFl}��UA�8���h�o�����=��
��q���=6�F��=����|Ons{��7�t�����=Vy������=�8Eb����5�,��?�������(��P~���)�!%o>
����������&��M����rW���_^t�g%�������/������?���kt���w���������?���Ws����z��o��7���^���7�o6U��_������������/?}�������������JBEc���*���?�����������7���x���?}���M'������>����7�K�X������9cc��?�����f���/�~y�3�9;�������O�����??~����~����������~(o4���YL_� �����9�����/������?�o�I���k����
���^�x���2�V>���f�}�����������{��2RH��B�����~���������������y��?������������^�\4������f�Z�w���hk|�w/l4����!	�^��
�.�I�o&y���J�R����dK���1{o;6��0V�>�X_k�1��
9x�r��1�a��-����s���4���
�tO�_2���p)%%4������8��yk#+��F����?��C�v-��b� 1���p�uo���imT��Lep���A1�����E2�[E5�C�����m ^���M�9'��R�.�L�<<��L����V��������t��m_�b
��H��0����KaT�y�J���X:@/�wy��NI�O���C�I$�$�Q2�q���C��*���o�9�b���hp�"���(��H����$�?��K��J8�Tr���!�$��-�����:���S�tRB���kA��/h����KN
��%7�W��W[I��)^�Q��� ��N9A�+�e�<�AtW�u�&fg�n�@�Hj���r���W�a�I�_�U 'J�
~$��elZ����������?�;4���������&�����v~Utb��!�	m�8��V�	]�����z���<h�K�w
�6������~�G����.*�'!�A�|�"E���t�b�&�"LZ�u>����B�������nZ:ORRK[�f�:�����,&vpR��|L�C����6+�G��zz*�o0�M�:��n7�`D�}2k1�AT�q����f�����4�s�B{<�Y���p��������[�^h��P��f��=���E!��0Bs�*r/����h3D`�L�a������r��yn���~heS"L���n�6G��M�%�����`G���)����XBQ{��V4��M��6�~���f�m��8,zS����]�����zv��&h'|�YM�T���} �����E)D��.��1��L��W�s���c�-������	J�U4������C{k}�����wW
kS���J!�$��UF�Z1���;��EI����DVH��-�+s��9��pe_E������[(�EXKK{u�������7�����������4��N��m��)c��Y%��������H���q��l��l������ISd����~V��}�Ez_�����2������`���$G��U`�gZ������0W������\��\�k��������S����T{�'$:��HN��8&s���{q01~�c�)��$s\�io�Et���/��x��ms�3x��o��{)���b����6Gl�%�EL�N���r�q��D����$^�A�?��3�����rng<u���J��0�T���a}�aZ���m�n��|�)_Py!�5�b)�G��)���� O����@Bm�!��2M�O�2�6�a����M��:�:Nq�a��	���YstM�4��� �hV�������=����_�Z$LY #�����hu�z�]p^	y����t:��@�Cy�b�#9�{�"����h{
���^(������r�:_z$��N�F)�6���q����y~!�baN$#�F�����h�'�Q�����R8ZF��R�X���'N8)V
�5�w� �V/��P&��Q�l'�4�������d���X��J�vd9O� BS;)!
��f�r���
e9TA'3U5�bt*p'����%U�V�
Ek1'3k��Y�����M����
�_�6H\~�����%��v��i[t_W�u)w�Y����gy3/[/H��l�����O|�>Ul�5b;��+}�A#�<��@�|�N�h'z�k�^SY;u��zP�D�z�Fi����m����	�������0��3���Nn�)L����
�����n�B��B:�D�&��B�eLsLAVcT�2ab
�Gq�z.���.�u�k]K`v�`��E��.��U[ElQ��o;&�&RA��@E���q#�{�?f�K�-NA&�i
��NhV��_n�dI�CJ���ob�Ni]`e)%$���j(&Nv>o>���`�\wn#e��DL� <��[Q�l����s�s:�j)=�ce)���z�u��:��)����[u	���:k�|�XY�9��~w���T�u4���b�����H�C��=���H�KE���(y!Vf���1b~1R�h�C[#D�ru��Z.�@W�9R6�n/��q���#�f5�[�,��PT��t�"1u�ce��j��"�"��7y����������G�S��{5�V��]I�d��}\�^��HY��$�� R��W�6R������c����}F�7�!afg�E.�-�q�4Z$�6��d��@1���(4����y�`^����T=�5��'�F��|DY���q�N���N�(l��O{�9Tv��Fz�.e��F�=�����;�t�u����T�_��a����u��"��j���+�3e�XY��:A�uV���JvXV�-;�h{'���PSD.?��?\��X�p��%b2���%z��k`R�L�E��lV��N?d�=/3����O�-%2G"�����m��:������'t�Q�iBw�cr���V�>-vLh�'�r'\8��t�a'����
A��c����������I�C�gG�)�(c����zg�Q�i-�tK	
�U��y����M����5�{H5~k�l�f6�<1�9��Uq3B{X��LMR�v��k1���UU~k�r@���u������E��qq
���o93+���VM."g-�e|-$:�MD����3��Bh}���S(�F��,�W�a��$j��qR��������Z�����$��D��E�$'j�e�S�$'��</���u-X��5�m��`	k���U�\��0g���rb���6o�3����yX��sWP�y>���1x������q�A������������'z�*)��������g���'���Y�����T}��Y���8�k��c_�d��W�����,�a/iU�;I�#�9�>��m.H�Z�)i�K�d�?�e��k�.j��]��#���%U2s=��!4�N��f���V�up������`D)�������n$��RRh5�NT�4��I&3�J�r��kD��?"�-�#iU��`,��M��8�8��"���'�C(�|"\�`5}��<Kd
������C�9���m�C�*S���*�^[?�I�;��D"���de��X�1��29�2b��!��BC`CV*���!��r��v5�����0E���|����2��a�����mE�\�u�,��{;E5�!��� A�C���@�j�TA�IH��Lg2��tm�!�e�yeC��z�d�+���l���o��Q�#��T�8%zH��c#mf����n�����!z��@"��'1�'�9�7�V�` ��T��Z�gSrz���^���@��kJR-&��~���
�n��+�0��ba�D�}������JJ�]���[��u��B���h!�t(��J������RY|��l���dF�����,ZJy�*��#���B�z��nN+K�{'��
��n��V�]����������y\OQvl��������{��t���Pr��X�C��3 �5��B�h�_�o3��hm��7d�,��y����"N�����3<x[�U�_u��M
X*�$�<����
�V!����Y�)���T��-�
���Y�4�:��;�EUn��ap��L��+z�Q���y����\T[��SU��H�����B�1A�u��T��pKTGTX5	��Yb�V��*��2}H��m���MG��(t[�����������X��w���]�����I�������Y��/������/�(�d�������9QLB���t�y2":L��'\��
+N���j��+N�8�n~�WJ���@��O�^>E[����n�G��z9'�y�z�q�4e�����Z�R���a�W0>e�hh������Q\F���6�3�]���O1�S����2C%T�(N�X��x��;D��Z)�Q�g{�IhK@�S�	������:]m�����6yF�<���UT*sj�����H�,��PE��S~���C9Z$'�
�[��a0��P����/>cI���*��MB��.p{��mn{���H��(N�*���%ps�I�S����p��C�W}u��."��e=��a�>"�l��#
M����z�0��P�.��#B=;U��V����Vu�^����Eu���W�Dj��5�������<YB[\M���C�e����`��$8����V9ot��,����N ,��U���]4�,�'M�a-)�GZ~j|.$ma��{r�M����&�22dI�Qo4��F�y�f��}�������{����uu��P���7" ���1�������M:.��S���+����McJv��d���r�d�"U���.Br���cXs��S�x�]��0�������������6�S(����E�O6�F���P��IOH���fZ�Gw�)���,)c.T>kh2m�a�� ��p,T0d���.X^�M
p]�F�#.����P��wp�^d4C�@��s9Z���S�������N�M�x�����5���i���f1��@���t��n���E7Qj�J�G�T��6���DV�}�u
=��8W���iu��!S�yj\�W�D-K�%��u-��������^������j���=�����a��!E2������!�C=��[�������q���LD�R�u��K�(�	�������1g��n�����W��A.��K�<��,�D�j�:��[�rb"u����j�����u��A�08TD&����������5��E�(����Tt��4H����)�\;O����	8��?�B��[-� R��m��o{��-�o'J��2_�p�O�)B=�V��T��3����8Qr0�g������3~�8�U�����Yq�q9�52��.Jl����$.�d|J�p����$�[�jE�9J`�^�N�D���(�Y�Wg���_��D�B�����o�C���pk��j�s8I'�va����f
��RqMn���QyZ�D�)H���A>�4���B���)x�"�su$"5�uY��)���l9>����e�=W�EL,;Ks�t���8�`�cGc8�i��]������?����"4���f,r�9
qh
�X�U��]���4%wW7�,�t��fl-��$��{��{���r��uW��
5�S��It���W/�{��}*W�[����]t}���YH��T�x
�Oj.����K���=�m!e3r��T�p��d��$6�x�Ny�QS��	A��ej�KO^��W5��ej�o��^d��>i�7�RI��-�<��f-Q�|BB��~w�����>���D���<|�����GTIt��������]�Y���Z�>��H�l>�:�4q_�v��N��B�d��p\RG��O����mS[�rp�)0��#�K��|k�4�}\Ll�V�w�������B��_��.�w8�s��C���������(9���}�C|��0FB�55rxw�.Y8�$��dV�s���$�H����z]IM]�%dJ�}��9y�u�?�{��c�)
�f����`����W� �0�v�;R�����L�`���8p���)����y<���o�����KII���}���z��Q��kU8��T�?��o����J�Z�OqI�q���S�(��XM0:L1I^<~��R<����Y)#��#�Z82�P�1��8�F��	���/|V��W�H���t;�I�:
���N��;���Ul�h9EQ�N����l��B�����%�������$�GO�!��
�_
����A����0�z{_��P���,�*Q�K�&��������9@�5�a�d�0�vL�K��w�����O8��?}�4x8]M�!|S3cQ*;}7��}��C�/���x�]6����l����j\����mD����������6�q�a��l��x}7h�L���#[�������	�%�9�"���W����)EL���=m�8�����{~6ZT��C��E[-���j���K��%����;$���*K�
m��S��D+u0=���t�����[�d�?�'�H��;��]vB�;s��eD�>\���g�_y����<Z}�:����xuh]�V�8����ZO����]��:���K���|*�K�NNj�C�n����&[o_<��2��[��3{h?9�=hu*��o5A�hN �����_�Q���GyE5:|���-���	���2�Y,�����WX�6����yj{�^��TXOm���+p�����R��
����^�D�.'5�%�O������x����<�����/�aX�m
endstream
endobj
5 0 obj
   12068
endobj
3 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
8 0 obj
<< /Type /ObjStm
   /Length 9 0 R
   /N 1
   /First 4
   /Filter /FlateDecode
>>
stream
x�3S0�����8]
endstream
endobj
9 0 obj
   16
endobj
12 0 obj
<< /Length 13 0 R
   /Filter /FlateDecode
>>
stream
x��]M�5����_q6�s�,����0���0���Wlg�2��}��nU���^����H]�T*U���C>��C=��F+�[�������&������_��{���7-|�A���22�������RK��~z����!�����/�����_��n����_��]?���������O�SB�}|(�B���������]?��a��~���w����.8�R�@���w��4P��PZ?������0"8���������l�R;����1��3�;e����G'Mx��Hp#�t@��sFhow8P�	�p�.��W�~e��0���j/��
���y����8��$��a fB�q�M<Rp�[$���
�;�p�����:O�q�"��u�tps��H�k=5V�:�������q.�t/Or_���K�7������p+���R	[������z���<r\WRE=��
)�ZJx����?�Q��`���_�h;N$�j��gFraM����${j�F(���M�d�����Ix+���_��7���������>�6B��rDt���j@�s#��]��,���Q���ec��t��r���J�Q�jX?��Yl��T�L1��,*7d�k��:�������b�L�h�nn�n}���105���_��V�_/m�iR��G��^��\�-���9
�[J�����J{�4z�TP�56983��|@�'�F*+��dGR�F5r#l;�����*����Z�4�4P��8�ed�2���u�t�p3FzG��r�7��Z�7�o�_�3�������N������/(V�iHd�8����&�1s<�\��r|UH���,��8����*����a��_��5�@�n!�&5�uH#���<��������N��cJZN�u�������=QF��r�v�8�"i�W��}�|$��xT���.J�����3ql��
����3��S���u|�P�+����� �����
�=�w���t;�^��}��q�	��]%��w��q��7��t����w�S��1��A�@�v�|-���8f�Q���0�rp�Vk-���xi<�vV3������`�#�?.2)x��1��
���;�^X��!��k_:J�2��9)e��f�9��W���~�:y!3;����]i����$�i��\d�����[ �/v�|������bl�N�`�u�D������i�r�K��?6��x�1�~�N���-���D-%�r�5,��?����d���dm�H��6@�����Fv��p":R�.� $�n,w���_����Y�H�g�����9|w>����b��b�PH�S"�
�I&�S�������8�g;.��Qt�j��;>/k�u�h��v�O�Gy��|R���b��C���U'��;t�Ugeh:��N�k(���](^���n�H���
J����lW�l��
����F�3c�@pB)}}���@��g����&��?&��} u|������x�P����������)^�F<D����L�����/\W(0T���A�8�_��?�3:��cP}�]�=^ �����:!�����4�:W�7�sd�1��1�	�1^�����0����b��'	�lQ�O���.���������TN"�n.4��3�D���;E@Yn���c�!����=��I��bi��O�)u�i���;�p�7��k�P@��9�u$~c�p�#g��h����RB���C�����?V����92"z*%�N
�1x.s�������U�T�@�y|��N����8	�`��l����A�QM����(
��{��!�T���~}��t�c����J
 ���Q-:AC�������$� m��t�������\����uu��
��6�8)�2�������v�s����B:
k��X#�,p�W ��[c|��w�DR��>G"�W=�����[�� �3'i�0�'lrp.m��J����j�l�?�68K�! -CK����M�1s��1��>qS��z�3��N��)e���;i����K�m����x���M;!T�N��b����m�Pt�$����%�����I`���r�'g`�O����Q�#p���NJ��N��	"b"��p��������O��KVZoSl��0�0�����}��CnM�Q$�&";zg&�beNTN�@�����$�(��D���	���3�6��H�k�#o|����:AA8&{���p��$�v:��������5��mb�:��R����[Gq��4��Hg��[Hg�r��j"�n���?���/������"��Ta�r�!'T�.|���l�R�Z��$���s�o�I
��3hA��3 �Q?Q-Q�T��>~�����#|����:A(X�'u�S-���(n�����M��"�Zt�
-vs����\�����
�n&f������_��q�����b�H�����b_�N/���X�Ye��X��V���7�e�:9H�v0���)q�Q4�-!����h�5NX|w	c�"��+Q����+���]��165];��_��xa�����	
<��tS<O5�2!���1�jo\������u���u����u�@�����p���_Y>$������	�c�]|a����X�0���Zy�F���$��4pc�!'�����!]���:q~c(�b���������d��V�?�*�)r�
fo�]R46%��k]��F�Z����M���-�2�����{��`��.���+���X��2����E��� ��d:>.����<��
�b�H���8
��:cNxI��QM^rd�N
3�Yn0�����kwN�ng��Z���q$�(a���d�7�88�;rH�������:c�bsM_���;2���I��8a�M���QM��_��S�tu�������:��
��n����V	t�y�}dl|x��lz�����-�� �?��BG�n���\�G�A'���.�����)2HORM7^=d���$imh�Mr��+"O��$�B�\���I�QE/�K��#cdS���[  m����:";�,X,���F	?�d�.mf,J\J���:	~����/�W��Ra���1C���i�N#n��@������(��u�	��O�[a�����nD����G��k�j��!r�2�G�!�����w�:|�7B�� E�a�^���k�������E6E~�Q0�G�d�M	G�F����@�1��:~�
<����B�f�������G���-��bD��j�����H��?x���1"��5�u|%2�w6�F��
�����++��X[��#'����W:��:�#��@����ot(������I�2zog��~i�-d��w{��
�-��T�\^6���Y����-�)��j�4:i���c����F ����^	���	}���F�|�3
d���"}��E���/���v���\1<���o� SL��~��6��7.��0da)�j��t4��\��QJ�K�:bj�"��t�*#ncr�]0RE�����4������f0�+/<��8�[�x������[i����F(Cd�]pJq�+d��7��$|S	g���l�tM�	�Jr'����v4������(�d��;�TS)��Ag3'}x#
�&��n���]��5H$��d;��L��;��uue�j-���v�tr�f����52I��L�){|��/<����;%Kv�
�+2��G7�\$���D����Xpx�h�L1�/����>G�n����xA�-��7�M�)���d:>������x ����s������#�a�K��@:'�����n�I g�����_�L�+�S��������<�&�����h�����m|�M��!��f�
>�wu^Z7�]!�LW���LWlZ��#�#~�L7����6e�4p���H{�9��:���<�+]!Y�u���;���9Y�8-�$3���q; �yx$�l:��Y��ndqx�$Ic������[n�<���|�����3�)���}t����v�!�E���t}W��0P��B	_9�����7�+;?F�xI�c�������[2d_�*/l�w2���d�N�b�gF2n�E'���;~�1}�t������.�Z���Sn�	P��]]��*(��hd:�b�Y���l�1W��%�����c�'w��G6G��{���h��1r{�$���<ad�j���Z,�! [���]�C�u
7f�4A({#o����ir��x����M�w_9�������������p3�D/���{��d���i��O�ig��1�b��Qtr���r�dr!��J�1Ld��f�����U?�����$�,\Lcaj�6��_�rv��pmm1���E��B��L����r7�.����$�[(H��
D>��#�?K�l�"�s�|�_f_�������9Rp}I+�3&;��I���JO!��0�M��$QM���JK�������L����!��s���W�B���z�\-��)s����R�/�?&���o,�Z��y�sC����#���E6En�[")<��e��#�3wK���#���Z>, �y��M>~��~����������{���r����o~z�N��o���G{`�	0����?HG� ������o�=m�7���7�@%mz�l�@���8l|��Z�rFh{��� ����`	��UG��-�Hfx�zB�.n��Hjg�N���iZv$�*�����@o��0"��T'��6������A��'3tX�����0�
>9�
��.I���+���X�S�,�����0.C��H3[�4��H�n�v��v�z#�������0!��-2D!������n��>�"������������0���h��
BE5a��=�H������Ax�&l}�_��;c��}���`}�G�������h�uJDe&l}��0�d�r��}�^��l��G�N�0�>�c���o[��
N�O9N����P��QQ	i�����c�VD5�o}���L��i����N���VV��qR7�g�����;�y!�G[9�����d{���6���<��
��p��G�1k#|�tU{��WG�������}�g������}�&
m������k�H�5#�>��k�8�[�px���G�G��m��}�^��5=b��=6j������~�V���q^������:3��=�c��M��G{,������j����m��vJ�0���h�o��	����
��Y���Q
������
��-���t�.�i������j��=�c���Jh��X������mudJ�=���h���"�iN�G{lag~��X/t����������������Ncn���&w��F{��*%��������4�1��������[���_~x����N�+����c�_^�����0V9��~z����!�����/�������������:��z|���������c�|J����^>�:D�C�=
��N��J`�l
 $p��_8!���Q	�
tpCD��R>4D�bOA�n%u�8�~��at����x��1����H�����?���O����5��z#Y}(c�V�y|�T���X�����|��7������?U�@�>���`�<U���D��NAs�,i��S���>���������@��"$���Ah��(��Hq�{�!���3�=}�#B����l }v�&��L��6�����4���;��,�������;�Ti]~������;��^����E�r�"j��U�h��
MjZ	k�R�r3Oel�q>�� 5��pMj�lP��=xzR�^��> &�q�`���E1�N�eG��a�rU���Hb��<�C�#o���/�0���B�%W�U�N����K��v-L�����B�����WM�����?
�k��rI���
<����,+�EY�,�\4�7�  �JM(eXd��^{Y�%��;i���A�H'l_&^�KW.�d��r�^�4���,�_Q0��s%)
��&)���Fu��ADIH�Eh�\9m�{�1�s�E����	>�y^9�h���X�����fA�^�twK��J��ba���d�� ���\WU�����E����)��
 ��6NL����Ph�
�d��u���RjM��*n�i���\S�\�%I�s��
7`�TA�*&B���Z�Y�\�N��6�m�+�6+�g�����o"OE#`=�U1��MX�+6��|�.2�Z���hg�F�O������&c��-'�����Z��-i��3k�������N�X'�n�	�:�Z�A6���{@l�kq4��g����o�o]i�8�
�`����ehQ����i��gk���$�\��/���"`�i���m��������
5��;l�6�_k{�S���!6�d�uc���x���1 \���[�=���l�N��D���NM���MV�����MZ��H������,��`��l����b�ol�����aT����[Ll����j'���~�������
*�N5��.��	�N��W;=�
�2�Z$��n�*Tp�"��v�if���X��	�GB�+5���OE#���+3��>4^�o�F�QRf��y�9jR�S��Z4};]�s�'�F�7�f�!���m71�(!k������-�1K��j~���K�>������l�+���P�E��yI��L6y��W�*���>��#e��$L�T��+�G����K���/�4��8���=��3�t6!�W�j4��g�����4=�mX�U��l@#3[�^����3L�k!�t�j��f����~\8��n��l�l��F���f�i�LD�BY�zv<��3e�������B����=�����wWN7V�MG�3����a_&6V]l�:��?P�<N���OAU�������W#�3:[|���x<9�0��Ts=R*~�p��As>/T��`���=`k�=��������E�'��Ls�J>
��|2�����2��-s;������u�zCr[�9����,�xKY�i��}86j�����
RM��JC|Y�;��@�����n�~�Q�<��1�3]�2�1����]9��q��59��0���Lw+3�9��e{O�_�Nv�E�d��]�hT��e��Q�$Y�Jm�z[O�!�[s^9U�2�6Q�[��g�?do�������.h3]�3���16"*��cI�����S��S&�a�Sh����1���������C`���,v<(i�Gb��6�����Ql\�hZ���26},P��^�*9���������"���]��d���Y��?]:�6r���Ue��/R�@������Hv���Q�������N#pz@ZgV,��sR�]2��B{h�WY8/�N��SM�v����<��Ski��
�B^}�	k�>�9�c�.9�i}q9�>�����^H����>�zW@,x���:���h\�|�m�'�[�`X����dc	�"#��p 0���\:FG���v@'m�U�r{���[��q
���_6x����ows����gj\��/�����{����?�5��aP'Nv/�.�<���8��=!�n7������]�vM�S��;�~��C
���5j�E@N��)M
�������c_�>��6x�J�s�6I�4�BO����]j%Za�����V�r�����N�J������L�G���=��I,Y[��S��xW��]*�38���$%�\v�}��N�W�y^����BW�B�6��1�>k`�V�Z��A���V�\�� ��8��+�v�(��0�`m
�=E'����NE��h��>5-@������NY	g�{���>c�~P���C���S�,K����l&��,��)q���h64c�<���Z�������T*�����Riu���������������e�#r���������#Z�SX�&Z�v�F��Mh�X��N�zf�i
��Z;��������kn����0����# e�����w4T����v���k���'�������TN��*k=�����ZZ���3%+�g�T$�+]{1�,��oe|2A��+�^]_�W�2�����3�����@�"@qf�E�[�#C^(�|p��v:`qdk�O\oHQ�FI�� ����P������LUr��0�}��������;�8l�{����}|8�\�#5�0B� ���n�GJ������|P��R�!-��@���W�@I��Ar���^z]��I�9�@�qv��
�}u3�x���}���JhK�.����f�<�X��e�U����gCk����U�|N�o���^�=)�-��9����8�"�R���;���,��%/J��:pe��Y��a���u�)I�U���1������B��!Ul��5�;M�t�p��]zg[�+D]	~��Z�����
"������\s^������<O��&�D��S��k(�	���q��TC��&��F��}���C{�)t���K�?��k������
1�����z�
�����*���n����];!�g[�aG�w��FC�����g�<��(�J�9�B���P�	��D���_>A��g�a%�r)Tat��G���2 E��+����+y�}@�wq����=��4d�m�&��?�v��[�������r�)��
OU��J��cGw,�<Yx<� 1���z)f�`�K��5<��f-,@n�G���
~��/wXy�]�����s+��D�������?<����|�3��2�6U@�����>�_��{���*-�T.X����>���������&o�����l2	����������������0�r\JT3>m�2O��-W���
K\+���P����|=+g���R`���#\���l�mx%����.3�fVg�N��������m���"b'�� ~���Hd���*�yo����%;�)�2E����n;����y^H_�]9U�D����d�B�F���P��@t��W�Dd��R8�!�i��wD�pAY	I���Y���\��w�:��k��%�Z����	e?+�x��z:��#0�����<�Z�{���t>d������|���K����u�+72T�������/�~5�v+�Q�C���A�ZN%j0�Dm^�)Y}/��Y����+��Q�kg�/��R���v�Fj�{
`������15���	5��r^P#�����bP��LE��NR�"
��b��)`"���BL���=��4aH�����
������C"�Q0[�J�e�~2�p�q���J�������	��<�"�lI��>�#�����������\��6L�o���8��+~�d��H��O�1t�XG\Sr5�d�AC���3\��'��b�bu�[	������V~��J���i:�J����kJnYH�:�Zd�]�R
 ����Z	B�
S����V�Mw�2�����A#�U�cD��������v��G�y�"6��Iah	�C��@���C�M�6�i�T@i�n�����>&�l���|�}�t��?�-�6%��x����VgP��mB��HLk��tB�Zv����^���$�w�rzBAjjR���,�2���l��g��I�
��t2��Zi���.3)��
�pTOu1�����NFp���s�
����^������Oh��q����<��V����HW�0�P����SN������g|�����"��e�����UJ����1mfk\�����#�~��6�
8;k'��g����kl����mK^ZG��~?p}�,������WU�#���ze�Y�U`jL�8���n�$�p�!v��[�o��d�:&�ZUKV4����/���n�g��=�W%:���]~U���N��x>�;�(m���W���8�g�t��}����G����R��%��^��k��r�������xA���^�mv�r$���U ~%�����\I.��f�t��L_��ak�h1�t�{�`����f���7��
M-UG������: �w�zB���}_��DQ�+�{,h���B-8��TV�N��
hQ�1}Cm����S`6��)�������
 	��XO����tN�p]��+�6W+���j>����!�wpn���S����h�KR�^G.MJ�T��j��	���T�&,fx����d_
�V)mL����.�j3�����Jns�:�fy?F[+�f,����B~�uH?��a:�.������D�r�E{k�,���s�>��O��[]�j��\�/WN����%S"�C�[�>��h���+��!���PV�b!����J��J"C���m�K��O��E��V���
�����ZY����"]��L���~��W�|�M��j�2/�%c	'J��!�=���X$S�����
[
Ta��Z6P5luj%l��.H�����?{<2�Z��smC��!�\�C�ZP�T�%nn���L���[��t�+�LwI�:I^�_�}�3��Io�#/�Jv�I�*
g�%����?G$Klx)��O�6�&��$����j����#����2W�zH1I��-��?eC�U>�#�NHG�;,tJ�y�4|q�K�k�y/��)FN��������Jd_g/T~p�������6�"���Y���z)X�������)�)sp�/�mREj�!<w�������4�Xr�k��xaG��I���7�-�pG?�J8�k����w��#Q5�e���S /[�s��K���5e�?V����t�hd��j��7���T42�cu�	����YE#��o	��vW&0��g�)y:����23�l�
�VP���2�lC�l�����
���B�rf�*5m��3&&9�u����Ce�u���'��=�2��Z���v�����y���c`/;�+��=OQI�f��w���t�5�['�mL��~�>�k~����fc������0or��65���$����+�56��5��A+1���o��s�+Pq�1 i��������]D��(��I��-����)��I�������
�����k� ��	�6���tLL��1��rP29��� ��d��%]z\��~!�� ���CP2���g��� f��@��[Lm��]�
S���-�!�H���l1n�`Y��ml�+,t�,]5�'��r�f�m+Cn*��v/��NE�ML2���B�N��"7bV��������V��W����k1���8�$�)�J�������[|T2	,f�/B5h�AI+��d��dO6*[L�������W�!&9�u_
z���T����!^NX��H9�[&������9H9��C���Ii��d.�f�L��qK�frYAp�<���F&���O�������V��!Am�Xz��Q��RB�
"���X�����.����o�WwfsI�"�>���Q�?�w��f�n�����C`��*��!���~�Q<G}�c�������m2���-I�����M3����n//h�CN���0�2�����Xu��5�����E����������1��v���y)L	��%�>���[��Y�FR�e��)>���ZM9[�"�-IMT�l���6*H��'���{�h?�����
���g&1-�2�����C4x�h��<R����aF=�LU0n�.<R�F%|2�G��1ZNuD."����co3K�0�A�R]���0�j�����'y�|�.�U�6H����X6Z�5%��S8�\��\�h(�<�M�
�����W�^0z>L�aFc�{��b���c^�aW�^.���5�h�+Ucn(&(��y���7��;%E�#�~)����R@0��au �������*��T"�-�q����@3�h����!�=�$��w���v���K��f�,J2�'��Zfv���)t|���|��L0\���%���+��*��N��%�OJ�����~�/_*�q��|�6�n�m���?��M-�u�|�dj	Ql��4y�g�W%PX�1E���F�$��Ch?b)�q�k��������U��H7�f�!�u��uL�x����}���V�JMw�V�/����v�d(M-m���Y���t@D������%p=gg���j���r�8�a?
�-�	���Gx?
D�`B��D7�Ac�������#�IS&�T��z���2��B�I���a����C��kZ���B-s"E��@�l��-X:i�z��?T���u���&.N.�a^��Ip���BT�'���b�
iX@W0i�T^��>�m����7Q���V&�f�������F
endstream
endobj
13 0 obj
   12248
endobj
11 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
15 0 obj
<< /Type /ObjStm
   /Length 16 0 R
   /N 1
   /First 5
   /Filter /FlateDecode
>>
stream
x�34Q0�������
endstream
endobj
16 0 obj
   17
endobj
19 0 obj
<< /Length 20 0 R
   /Filter /FlateDecode
>>
stream
x��}K�67�����wSp������mh
�g>�������n��.WA���A�P(SR�����i=���H��I�^�%_�%_.9��sI�~����o����~~��{���oo������RT������"���}y)�4����_�~�����/�����w�?�������}}���__�����������_���������&�����{���1�?�����J����^���~~9�2����A��F���>�~�������^R��t���KJX�8\��+'��tKo��^�
r��x������huJ���&�����q1
�q�����	n�&#�����u�S���a���v�P-����mG@�H�J�d��a��I���Z��)����.�`�����}�?���e��-�H�E�������hg�$f~�"���n��Hx�m��6K�aW:c��}:sIh������U�2�I�LP34u�_:R)�m�0-y�(0D5*P�� �����s�P��9�������bgp��-��&i�?*L���d�>��W7�[�gW��*s��JfZDn�xt�o]�
�y���H���w6�H^Q���J��&���we�j��'�����I��M����M��~��U�c6|�7���1��dDTh��[m�)~���cp�]�l�)"dB|4fR�������"6���C����	��p��Cg��	��"������&��7*�|����Ho�	�(�mI�-������`q���q�eBv�IP�g�rB���W�8�)������uA7������4�|��7��9x�.�����~<��E&�J��P�=q@�nPMT���0x���i��v�b�2�
�t^�
z4>D+�P-R����+e�v��pR��zFE�����;�L����}�w��=t6���P�pW
%Et29=�@���g���P�QKoa�c0{��97O���N���X��~���
}B1&�X!�������Nh%��H��S�'���3�rC6�%�����u�q�n	-LC �7��W���hK2)�<5�?�,�q�H���A��m7�c�R��L�����p3�3�no�bp7p��Gu�-|\S�(3���/���z��w�����S�A���$l�����p&=�EA��C�0\�7I3]u����hx�:�{	%V;����5��~
�Z�D��C�O=�Y��uF����p]-�W�3����]aU�s�*p��>3�������E�Q�)��l������M��x#]YU�xb(�@k��?�������J������\ ��o��6\X�6�JK�o0�X���rZ(������
��l��8�)pD�� :U��9�)�����]�����H�P�!���C�1��/��x6P	�P�5tS�X����� j;�AjJlH�����W"j��a�hx
��MA��(��>	\�+W6o������w�(2���z� 
OY�@��A�������=2��_�0`���y*A�.(��!|�^K��!u�H��:x�w|���i�x L�[Nn���� ��Hxj�r��oJ��M�U9�3�����E�_���Sh/�����������
**��
0n,pJ�75l�\��3����������=>�+�����������J��yD���9Q�h�����L�g�2����f���~��A+���F�/�/�1�����0��aI6=�����
�:t=�A@nJ������D9�\�d��-��D4Ti+�^rh|��J�BF��0�����eGn��D�L��{R�ge��,����ZJ�#���q��T�4��[���vvY<W��H�l���	���t_�?'�%*nk*l*�*�p�����(��q~_4�
u���}����o�/`LIa��tPE�<
��:O��@`�l��U�X�@���������
x���crg�H�����kc����-���(^�rH�!9$8�z���;5�!���;]�#�����K{���o[|�'�1xNE���i���Z~���IDCT,����>
~�)���c�����0z�l��g;�!�v�}AVu�@:�>����1�������8��r��d����$��M|sd�#\��;�x]���uy9��M�=���4��0�������;2��';"�na>&�3h���1e �;�����R
����O�|r\����z�$���,�j1�L�8�3��[��`@�T�!>�b]���W�����P�9�@���Y�	$�_��S�����i��EGQ<He���g�����'��������u;���=�p���7">)��t�8�9�p�<��^�";@*���}��$B���`m,��D������.�_X�5��)��#�h<��Rx����p�3s8��g������N��nF�,�`D�����.w�I�n1r��P�wL_H�CV���x'��<��t�~���~_|���a�b�n�'�����/���=y3)�!��������1rh�R�D��VO�4h�Bi�zqV�e�(�=B�uuD��0=0���e�2�����Y���t�2�w�WT�y���yu���GC*�6[[��uB���x cA�@p�@�@&��(l,g��x}1]^�p)�h��gF�:<�����O
����@&�����'�4�'��i5R����X�O��^��������ca��d�q���	�1`�n�s�9-:y�/[ �t�9H8�w,��s��,�@z�M���{�	�������Y��4��M(���d�1_���HWa<~9`w�k�	����B' �j�%���Cw|#�j:Xx����@
��[���x��H�����O�`~%�G��8�X�M������N�Ni�����O�x8�A
���{���-���h#�(��(=�I�)�t"��J"63�������h��G���,�A�A>�l����lR��e���[]���)���������P��2��[��Y�tZ%�P�t�@��lq��?�>��]����q�#����8/�&.�����?
�����h�;��,��l�� 5%L�����:�H��G�j[������#��Cc�"7������-.�/:�0��B�u���	�h8wC3p���8���u�4e3�}�PSb���`61_��;)L�u<��A����C��q�q���^$O��z��V�����WoT�M������.B��Q����p��������/^�4_A����]�4n���Fi�;^Dx�U�p�?J��{�}I��~�:s���������0I��6t�Kg�x��h�����n'c�������RXM��d�9�����TT�:��1�\�Wo�r��c��<���Q��_��*�@&����7%vu~�Omt�����l�� ��S�_g�^����L�r�
�!��~�f������������
���yZ���(s!��f#o!Y������_�����G4s����
J����G��3���j��}��X���*B������-���B�������.�� [���������Ov����M���sI�����Zlix��i@����=Np�����i G[�,_��Y"���������
�cl �`�n�.��L����������fp���8��N��o��b:��x���;��Qr����T-u����n
��`�d���H���;%Gv2�EPD]��=�4~��t��� <�
�����N~�R�A�I"k�pa{�����H�����=H�s_b��1�q�������"�LEa
U����k�;_S�����C��XNn����sJ~K	;HfM9#����/`�i����>�,��E�Lgi38���t����<���C��Z��V~_1\O�"�@�?������7���z��{��1�Ev������r]����������c��������RUb��1p����5{@\(,/��r�����h��>���]��jy�s��t�|�e����]�}������/�1����N~W�dS �'7 ���;�@���?H�,	A�������&��8n��9��9?�� �w+����r��u��D�6}��&'�����a���Ovdb��L���W:=�Y�h�U5�MT�C�x0?���M��;e��Q�I��*/���H��p���y�Gg�}P�q�@�V���&�Tr
nh�u��M�[��!P���K�����88z��mua���?G?r��?N����I�J�x*���f;>�5�x��������[|�{�d�o��f�����O������M1�-�*d�~X{���4������R�E��L��M1�M������]*����Lv	���}��������!|���q������A�x7�Eq(����������%��0�����8�u��M���Y���3�[t�8��#4�w+������_�R�X�4���o���7��������B�~|S������Ti�Q���/o�����[\|�����B[������-W�E����~��FT%^9~�?��T��w��`�Ix��
O�Pg�b���d
Qh������.��$<�B�9B0��k!�7��s$�s^�N���i^�$�)�"F�[`p�&����jr��X�';���|�a@��E�c���*�_�'�_a�B�,��v�
<y0��,��+�d'�c(�0��6<�����
I�'d}�-/���#&!m���d�g�"����d�T�c<��G�(TR3�>�c�������&����2Z?��=��k���&&l}��Z-�����=�i���k{t�
ezL��h��^�iF�'{dPe_�����a������v��G�~�����GX'��	j��s*�/��?���?
��[���	3O��h��Rh9��`�HZ���h/+cE��������$���m��Xg�Vs���6������V�7�N��>����jF�G{l2B�i�G{vRi������r%_��^��*-��F�`�0����h��� 0�=�J*_D��<��h��������h�u�c���Z��j��z9�f�n�7�����NM��?��9:a�����v���_Z��j��cNN�8�o{�sJ9;c�#�9��n���(������ke�i��p�d��G>���m��o�����������\K?)��d��Fx?�������A���(���n��-����=6j������)%�~������F&l��7��Q3�>�c�����n�+���_������sx6����Ah��n�QX�|��������!_���������M|��~���?���`4�:�����w�&	��|X��6~�01�wD���
�D�*��[E�����
���h��R�"
�|X��"��E�J��R8=�
�.�S.�n�1I��0�]I7�T���hL>�����}��&���Js���4�<�=4�md�6J;�c��e��`�jc����h�|R�P1���)�����7�)K����
�q��: H,j�
���H�OUI�+��5M>��8>L�a�q�NV�Y�M�����IzKR�����V\��M	���}vkbv��~L�.���XWZ
�m�@L��H�e����0��Ui���m��a�1�!��M���i���-����+�]��sU�I��{���ied������PF	�Sv�/��N�Z�A	���m{�<{#�N�Lh���:�Ix��)�L5%r�����g�41�i�[��{��W"E��>��!��J��	el����:1]�(��ub��m|OBe}�\�/���D�{���
`�^+�$���K�^�����%�V�LQP�=�"{��~��x>��I�9.�Ou�R�rk��nC�"��2���W	�����������|(k��j�\�	�Y��T��d$��������<yX���8��������P�gbP�9'e��r!�!��[y��<��If:�7�T��@b�B������_����g�����]�i���M����)e�Mr6��s�C�em9;R��h�"��,���h��.g����v��`B����"g7�lH9�I�o�`��u�3��*����Ds� �u0M��� �����o�A33�h���&�	k�Z��b2�0Lc]u[������n��ux7l���:8�����}|W���x��������e��bA����
��V![�]�X
p#R ����a�N�#��UVT����"�AZc�0H�P��=O������<�#���HX(,0)��WrXyWhp����cV9]5�B�Nh����'>��q�g�d�;|!�i���B��Bq9�h�'�����T�����������n����]��*E#��H
N�l$�0�}8���:�$��a��!<�T��.������2����~�i�Y�9���	� ��dn|HN`��0G&F���\����{_H"����y�7�|��������"�AH=���'R;'���$
0�u��m�B+�f�����(��\�lc��v�0�O�a��P��g�y�6s�"��Z)V]Q���^E����,��5!_�0	=�ny�1s���P�v�eZ+�
)�T��s��@N��k�{X<�b�L�\������0�<h��D�B\L���&�]�D�9�����dg�fk�����4�e �E\��B�j���(��L����
v��|�ccH��CB���d���bj(�g��:�y}���c�`��b��H[�/J�������QR��Xl�C�	���V��DE`'��x���m8���{��G"�8:�@�L���_��'5�P��iL�H�+�^7`���o�$v}b�~#ae���VC��HY��D��%g�P��6�H���k��+��-sB�$��#�BTSV"�N�sF��
9����28���Vb���=Wr��fik�&��!1�-��6�]�H��j���D��C�U��nc
�\Rp>�V�����Zk�x�����\G��1sE����	����&b��D���q������f��$��g_�����.�8���R���������s+������T����M��j�1������w&�o��I�d�!�J�tK��(<����:]Cf%Ihc^��1m�$���M�%_tHZ�s�����I�gb�gZx<d�����������BEJ�5t��A.��%����u�N��RP�B�V�z��4�k��t�����,8�V)�"/_�$L����&J
�u��0����W^1HFH�D��<�"��C�(�w��������3*G�����'��J& (������I���`J_��G���I��E�����`b���n�`�������=�����&� !� ���l��"��Q�j�%�{����}YF:��)M���U�����jbD���+���QK�����n�*�X�;����%����^��X��cu�x��0�>/-r��#gv��BE[L�l�`�Y�LW'f�#;,OY2�f�ZU��l	tR�]�&�:!��u�{5��.�<S����e�B�s�b�e��5��AKu"���+�x�K�����&��zT �}�R7�Z�����1/T���h�h���o
QM����e+���us-�3+o�$a7B1���^�x���P�P/_�� '~�*:��*u0eP��[U�%7�q_GU�m�	x����/KD5����<�S�(������Y�����Tnh�y�QDT����i}���H���f-��4��T
6�P�������r��z�yoc��B#*��d%R���)�B�������1x=���
�q�����]�E[+��8�w��8�����.w�$����z�l_��	���
��vk�[����"�R�c Q?��:�C���gB�S���tLg�,��il��K�y�9>��%(m%�X*%���mn��x�H�P GV�j���]�.]A8��������l�S-Jd���IB�D��H��`M<�\�[9��q$�S�9u��L�����-����i~�+y��������+I+��e%���*���_m��9�^�����RA���4��1��^�Z��~o�����g�Ke-~�#(�5q��a�6�^���xc�"3F��9?j��g��	�j�h�`0�/�s�tE��ba�hp��%�>�u�b[k-\;���`���	��v����sm*����-N�;���B�T���m(y�����`X����X��8��h���V��%��3��������
M�=�Q}�'B-�������5D\&���5��������I���8�&q���f|�'�����;K�C-��VL����I������������t[�����2l�cD�L�BK^��U��Y���7��~q��_��n�T�>W�Q�)����L����u�����UE�Q
m9���2����������C:�+fv�c���m����,T�.�}��&������@/�W�C�k]�Q�X����F�)��ur�����
w'BG���S2g�g�����-��Hx%�Z���"�.�n�t�]�}P	�
�Z�Z�Q�����P2I��Z��u��*A���Q��J9�	m\�lV&J��q5Qy�\R�aJ��	m|�>�R����@M���M�e���1��"=���-��:�[�fUj����R+9��
g,�5������)���,��R��)��fn��6��-����7�~q~�[��^T�����{�O��wy�������w?jR���AW�eSy;���-l^����,ok�`�B�^�����������'!������	l��-���vDIOV;BV�&�#�u���[~��Q���)������1C�<������[�'c�(�����4��"��=������?��� t�P	��x��i����5�`\����}�+����WJ�EBf�(.���T���'	�	��+�8�o��v5	��bs:��
W"%j���#!�����B�����������������O%*�.L�UWPe9����*���I-~#�w�3?�A)E�0����x���wa�AQ��|!�=�c�\�!�\�5�X��8�������S�}W�}����%fyK>F[�<�j(���+U:��N�K���R���Q&�skk�'.W^�BV�GS\��ax���y�k�K��n&H7t��z�G�*�I�S���G��|)���o����[	S��}��5��ql���Z��J(g<N��j�;�<����x
���gu���n{�&�|j�`5�R�.^���-T��OY&����+g�U�@��)����\s���P
��1���Exq[ �m���H���@'���N��4��mV/	�F\��~��@��B���d=,nL�pj3�,&{A�~���I�����<�c�{�P����Yy�m-v��������k��@q�YIe����~fb�Y�z����:���l�A����{8��t���P�PQ��Q:����W���hm-Y��3����r���H.�������V�����jqIg*�*�Q���y�E��B�'�c�L��|D����N�t�������t�RA�{�3� ��7��4�����3�P�]�24s)Ra]�o��LnT�Q��?�B^n��~:��-��H�	r�'*�RM������[���>�9���]���b��s��@�{V��3�l��� &lMi���D�vC���I>O�����i��%c��j�|g�����O�O����(�����Gq%���CVlZ��v�[�����+��[Z�j�\cr�9_��\����o "6�����p����<1�%
~Y2��4�P��K�]����V:VJ�R$��*���qL�/�N0����&���4w�����`��`+�:O�= B��G[]@��dn3�Q�RC6%�Ln3�q�W�8��[���2"WV���8����.F�%�|u���4�L�-	�����?�����t�ys%���U.�����G�vW�����"u]������e��/-�l�S:��"�}*���l8�Nv����ajQV������ZQ'a���

:D��L�y����h�B&� evO���VJ�[��M�1��$����W��64���m�]����D��:�ZU�]���u�VR�#lx��%*kmzY!��>81��T����^��m��D����vay�&���^���6X{^x@,��$��
p���E���Q�QYFK�m;ZT=�a*Oi�E�Y:�8�m7�8�r��-?������}����-�8N��+��1t�X�i���C���6��h���ztD������
��p3�B��Z�H�d�8]�����*�r�Q�����nqB3Mu9�O������k���J�FrT=G\b�*�y�U�X_{�wY��M���?���~��������K��s�Kw���*�
�x��4U�C�����|����wZ7��2��x���yo����{<
������A�!U-����:$c�!��,����o���)#_V��6�[^6���~�A������������P�����U�BK���Ci����?��L��AG.�7��������"C���o���~�<�D.#�Ku����RZT���\����u������c�]WPlp����2����^B�I�@��q�]�������=
����
C��T��gi����z���XY%���X\���h��2���@&4s?I~�A���U
��]�C$m��J��j�6��.�t��)
���������S���Zh��/b�7��8Z=n"�A�,��"�����F��u���r��JJ{�������ju�/���"�
y����j�LL�e����!H�G	����O�[!��)���)�i��.e��@�~(��M��vsz�x��C�����������3�ni����A�o�06��W"����v���9t�x��M+���y��v)�m.ooF�Ic�Go��OrFFc����F��Yd5T�t����u>�l���������$��]]F`��j��t�2<���Fqv7����jaC�y��!�������UB�^��u<�Te�f'K�b'��c0!s�E��O�Cg��U+&�lW��_e�P���H�3���=LjP��P����xJ:�K���6��VK��V�ie���FS���>J3����D�eJK-{88�;?,Q��c=�V?le���#����$��H��\�����A�	��5�����a��A���4�a��:�f�S��ft3
���nv �`����,&4�������V�:��I_r��	�V��Jg2�|o/�/p�u**���������N�[���]�R*�Ul�;t�����N�ilq�o`qxPD����"xg8r��b�vE���0�Cs��W������!D�6�5S���T�O����/�����1���3���d"D�
�-ft�����zUlH��
>K���j���r���Mk�|\��>=�����|�y}>y�������I�X��ef��)�
�L�:�����P�U��<��M��
�����z������cJ%��Hj	��c��e�M���sB�i��;�
������w�(B}��iL�������o��:��i��+V��;������������v4*��~�g>�������LV�����/Y[��y�'�/��]�c����`�`�95��FD��v�6�� ]��G�����l��-��[�D��;b�g�m�9���4N�;�
�C��Q�ZP��{h��K��!�%|��JA��E���F����D�j��7�U3N����}���A�����9�k�.�!T�nP�a�+���8�8���d�<y��d��
:g`�v���"g�w��-���_��o����n�����%�6U		|���f
`�>����G�I�NO��q���n�'��[~q�Aeo
��P�G�n��n�c��f|ci�BLy�5���j4��j�������C�������]����J��el�����8>�������G]�K�H��%*����

]h��	D�s�p�-@���q��9.�p[8r,7GLr]������k
endstream
endobj
20 0 obj
   12115
endobj
18 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
22 0 obj
<< /Type /ObjStm
   /Length 23 0 R
   /N 1
   /First 5
   /Filter /FlateDecode
>>
stream
x�32T0�������
endstream
endobj
23 0 obj
   17
endobj
26 0 obj
<< /Length 27 0 R
   /Filter /FlateDecode
>>
stream
x��]K�.�m����6����c ` @w7�b0��d����T�H�J���^����'�DQ$�2/���O���P��)�b^?���?o���~��������}��ooN�R�)
��uV����+j���>�������?��~�����~x���?������|��������?�������(m�����\�o|����~�p������{�G����f^Z�����o��BL�8��^�d_�+g_^��^�������iUr�:�pk�=��(�
g��&������-�V:��lO*<z�R�1G�^�w�
���U��8�oG_3[�5���=�K�iy�)�Q�u��E;�XT�	��t80�5���\
�j��Ni��_��[RA_yq���J�������Wf�.�\������[��
�d����$��`C���a���[.�Y.���|Y'�(�#+�+��w�H�qgR����Qx[
]@��d���-���9Y.<e���a������f1;���b0������rP$���^$����&���m9�1H`��U�f��6�8����$mn&�p�����:����~Z�m�P��~��V)D�����4�*NeC����-6���o��(}YR?��&��d�����:5B����9��0�O[�����<~�/YU�s{�
2����wZ/OGl�8��q�NBq�����%�h��3�g�`�R��?�%u|�k3������/R�=�q067F4�	���+k��CN��f(����f���_4�Y�l-L`A�����d&�f��^����q���o���3'\K�D��.�����I����A.��oVF����.���*�]����7v�<k&i�m9;C��KJa�A"����6IP�P����5�wjJ�K�.�GkU��,e��<d9��K��(t|�q3F~%�@���=u�[zr'�%���y��I����X��)��|�E��A|G�"�%=��M�-0Qh���8�b~n_�8�����yB��M3P������QF�;��h���`#9��vIW�����[���%�p&v���G�D����d���Z@��_��z/P+I�?���*Wxk����,�:��+�H�B�g�)uu�Y'�q�_g�`w?T��6]�%6w��
~�FN�(u��B����\�[�2�v(P���n�0��2�,�E�K�Po&ES	�:e7SY��l�g�>�i����^x9���������w`���gW�v/b���C;]�{� �Q���k�����,�9�Z����� �;1T��	9Q����1�����������2��g���@�b������	�����NP���s���k�e[�@� P�J��`�U�G@`����cK����rc����CnBwb��8A��)����|�����M��������9�&�"������?����5EHfvK�4%�l��*K����F��b��|6!&��(a�WJ�� {��d��~@	mz>��d����jP���
�����A~c�g}�6E��-@�\��)��.J�4e�0�h]�hZjK�v^��������u%��cmr��:>�y��D���0F��8�z�1�F�g����L���e��L,Ae��u��/���7�`�B$?����,d���G�!�2�)Q�K������f��[?�I�g��N5���9}F����Z���{x�p��x"\��t�y��`���P�1�
|c����Zk���B*x&�SHB�s����7E��-@�|w��r���J���dQjB��n���m�D�����BS]�6)'��h1�z�EX��<����U���a������/�@^P6�;�5]�;3��
���"#������Q '�)�v�a�[��Z�f����BG�@�d)��g�n��x)�8���@�o|Uy
;�����M��WZ�ib<|c�I��R��R�����h���pK��D^��p\��7����s5���������Y�(��
����F�(��%�	�]d� �E������e�-6@�r�����������6_6?��72�;A��
�����������grgP<�!e�E�p���v�y�<�,|��c<����@�@�`G@�G �K}C�#H�3��k}Qu�X*���y��6x^Yr�;zI��cC�*��L��W����k�k�>�Wf��-g��>m"G'�VY��Ui�7�����	��\��y~�,7��Oq!�'Q>�O���7���RV�����F�;�(vpN�������E�2�^�g
�����*�!������b'�����@�g���q�'��9���/��5E��0 a�����="������p���G��&r$��rb�;O�<�1�)p$�Bpn�<G�[(��oq��7��v/�A�K��*���o�^,� D���ec�'���}(/r���[��X�N��Z�����x�Y�q-�
q���p�N�{�n���
�-�LV���V=
�L���/A~([�L�.'��*���0�j����rG�����N�8E�]
�b���`a�X����	�����&g_S
�qh�DJC�����<��*�_�'�	Me��r��M��E�t&����y�|���;��)t�Mit����h���C\#�c�t
��K�c���3����2�����%�m���$�i�������5���d���v����w�LM����HK�H*��
�k�=�?�����o�
��&�8�eeIMIjY�c"J
iI2������.{��!R!��@���
��'����T*Y��J�)p&��x/��(�'��YVO��\$e��y�b��n.V���]������c�(w����[d�7��M'������y���Mb�#t|&G#	R�
/��q�����:
|��!������gV�
��CR%<_DI����y��#��sE�"��<�)&���d;��Q���V��P~Q�<H�8]�BSYU�@��
�-�576������sQ%�R�H�b�I#/�pMe���RE(`H��F&���-��4y��Q!2�Z����4��Mr����Un%���Z��\y��S%�����V���*�ug��Qy�N���[�.������H��^��Z�G�i�Ms�M-�[}��7%{��k��u�U,��AL������h�H�-^��wq����`��_H��H�,�^Y"/U,$Kx�`��Z��/�\S�n9}��l�Flb7*���n�\��JU���Fp6Ui��dk������!x�������wD���x=���G��c��kN�����2�����7Y�7����[��7��7���w5Bq*k��u~J���P�����TP����]Ni�:p��8�{q"<���u�q	��J�S�Us����E?�o�����'0/�`������&��r^$	I��<��\��3;���
WH]��N���D��vR�'�0T1;F����SU+r6�}�	Y��.��K]�sv&���|����dZ�`�������6���%����`K�G.�A8�����s�L�tsgx0!�q��1R�w�X�c��?(_xU�h�u���|�A�� ����"���C��
���QX�FWHi��.Y����he�jL�Rr��'����yE�(��[�G�[�3A��A�f���6=�5 '�u����-�A��<V!�U$�"�|����x|���27N.��4>)����
�x/Cw�STL��P�(��WM(�$���/�BA^�W_��"�-gB_�gw���W��'�:�|�����*�o�9�V�U�#�W;|N�M���+�~�v�����4r��B�����+ �o*�o��<��k[��5����)�I/��R�Z����!s�U�����8i��d%�����xS��O�P�!"I^��1�$�O��;�^K����$����J\�������:R���A���x>��5w@6N�o���V������[����V
���N�TAo��ZL.G����M:x�:	�/��8��}�cd��w����T��tN���r�<�V�t���W���u~��J�h6c����Aa�w
��:7#�@R�B�rI����J�M��ODE �9�o�N	���������>3�wKT1������[F�F���)���n�(����Ey����:�c
�Q�9�cb��N�1�93����_�>+����5(����
����0��aH�(6����9tk�������T�{���@b���v��
�[�X�"q�*u|��"�{�������|���x���zc���LxR�����t�X��wY�R�$�+�oXO����
��W�`�������X�mg��F��f3����<�����8��]���G�SA��Z��zf���X��U>�c,<��bw4e�wi;����^�H���8�,���v��oy����3}|��T��J�+�'��1�d�@�b0�t���<�I����n��Va�)���mjqJ�s�M�W���������8�38�!������/�N���@�z�qu��@q��`�S_�\h��-��#���������3��S5#��zj
���w�y��I����y><����|x�U���YR��W����-6@>gw^�������D� "�k-��b�	b�t��m���2���w���P�?��lw���hS&&,�O��%)m�Q�&)7+��9#�?)���Z�@�����
���wnV���^��xz_�v�x�=����������-xx��!���z
=��	<���c��@���
%'��h.�&���{����(|c�|�'`�2�t���y�<�W	������J��o�\?�H���R�t"�R��L:���<~����H��7�����&�w�$�G�77FZ�����	�SZ�������`e�2����:F���w�����"����q�Lk�_���_�3���/����oF����?�������O����b)���U
���k<6*���?^����zs-�j�����7�9\�����W������-�-6<�B��6��'[h����v@��Q���d�QU�����	�-!���B����d�i0�d�����/�"���U���
OvP��!3����,���Z	IYS����k����x�_�'����e�~���q*�L�T�s�N��d;a�W&O�?�"�SEO}�'[��Y��(t<�cMV��	���������$=�|����������`�)a��G{l4�����h�M�������(��~��=����o$i^���~�7:G������C�e����
l���gl{����������-J���<>�U�9����V���������O���U��l������<�q9���?�����p�f�x������>���h�5V=���h���@Pl��S�NW�g�x���G��o��s�b�����u3�=�c�Q:������������~^��b�V�x��7���o�����������<��h������m�����W��������`fht[�c��m��X������h�u��2i��h��4���1�G�~�S1�c��n`�
�
�����K���G7�YY�gl{�����2�o���5C"�Ik�G�~�Ua^���
lR.�r����b�����������>a��m��)��58������~�����33�=�c�f>%�G�Xu@~���w���������?�emR6����|0���_�~�����/��������3d���������������������������}��Y���r��+��(�l�aRr����n�VVWtL�Y���~����IC��t���&��������}�Z�?DR���D��.B0��L�Ai�7U��R��B�i����\M��������K[��R��6�46(�|��"S��'_Zs���g�V���B�IR:[3�LT��
�W%|��{����>����/�M�!��a�
)�������^?��;��y�'��,t`�2Qe��Db����Y�Ah��ES�t"��#��xeX������4����W�U����D�J���Wp:��}�D��kUB�z��J��c/K�K��c)����a�BPQ�����p(�Z�4�dw��y�s��]{*��J��g.�*���
��pU!��>��V��d�c�19C������aH�yS�K�c�����5��	����XF}d�����pl�+���C��x�l�����4o���4��V��i=OSh������*���k�c M� ���&T"���[s#k��:;d��c�;+������T�������Ru�5�Ug������K�0:�n����$xR�mb{�nt�k�C�������T�w�H5�X"��681m��J[fV���{�J�1���jq4=R��z$���rRg
�#��G*���&i\���BD�)#�T�������v`�$�9�T	81���!	UO�dN�����N�������g��4���1-M����e��!����i3DU��}���;\�E��Xo�[j��*g���1Q].���j<���%L���B;#J��
�W��v�u/hN�DP y�@|Pm�<���J�����]�Xm��M�������2#�&�U���#vH�2�6�\���!�}|�)�>�su��
dH:E������v�E�����P���Vv6�x��X8�O�KVn�0�-7�
Q�W���]�kc���Ncr;���M3�����P�_v����X��gwX�-����!8�������{�3�;��&6�|�n��A��1�i���cv��l�!�8Be�#d�9�C�#daw	�!rLn7����/������#P���m[������
Fo{���������b��C�6�)�8��s#�C�Z��4�P7C�c��aZ~*��93�+f��!�W Dj�Szr�X<,���k��/�������pm�@6���}l��JN{c�����uXW;c�yh1��>lk������WJ�d\��;��j���m��:W����"
�:�!�|a�tWGJ'�VNZ(���3Q�0)�,������SK�������4�r����U:�T���L9��y��v~��G�����UI�����Eld�'94w-��+��*��M+%}R�Fw�6��5�S�gA�0�O?���?�����:��;��&1��T�V������I�����I�n�k�n$��b}X���5���>���kR�t���m������G���3YW������1���U�|Y��1���qN���u��tqH���h�z��J�X@�w_���iN���N���9hN�lBsU���zYI�������FkFa����7dN��2Vo*���)8����8���M���o����g}Sq�v�M���5X�'���i����@K�����9'���0�<8�K)>��=������0��Ih�(8o�������sD����:>n����?{�0T�pl�$����~	fw�O�Hf%�x��h8�������T��
���h���u�_���r�3�!nS��1����5����c:����V���q[�*Z�k:"�5�F����a}+ I���>7��v����227���-��~���xe3�a�S�c
jw�$-�RS-��'x�po�P���9����n_���x�t�����.�6-�I�ib���t��z�$lU���#�0�H}���2������6�4��|f9��W@3A��	=N�ICF���ci;^_��	�Fv�I��9d<)�a�$=�/��)�{������+�y��/)>_���4������L������9������P�s�l8���H��I��F������cn�;}#5�]�s;i�!�����6�����s~0�����6��3;���Uj(���P��g����p�#�~�������=��G�}f5�6
�3���hmz</��I|�������iR��5�:W8�tHv��Y|FY�q=����$d���a6l\/��{�tW�;��^���~�9�z�M����nH�k��0�!|�a9�'{�L�������9O�<C^0�k���Q���7�X��D�~}<JQ���C����g�NCf��?b�7%�
������I7������>�����i�a���k�8z���$�!��.l�mw��wx�j����g�x�;��t�4��#
1�}�Nng��TQ�����{Fq����*�����c���^�0kv�����E�W_TL�I����2J��,��Y���S���A�lwN�3���,�c���P7_w�0�����#_j�y�*��F��M�8V�/��_�a:^���Z3�
�l<Rl����h�no����x��������'�������ji'F���SG���'I�&9�h����g���ng8$&���f�v�b�i�..z#��]�����'Z��w�FK�q��#C�at���c�G.�/�-�|G��q�i�\���N���
�b�k�c����Zc���N�f�l�t�dcJ;Nc���m���p����������$'`����Y��0�c�{���LF�px(`K��%����
�,�����|����>AL���I6\l��%w3��5�#�����v���L[�l��������&�$z���3���s<�1_mn��M�&d����*��#7Bf���a�D���VG�n=H7�*���Mb�2����;�
#�KQ���r�kM��yZ5���?
K��m���/&��u;�Z"�&OS���6�{Kl��qP����Xo����6#c��c/'������`���#����_���s���5b��M���B�������xo��ku0�u8}d�m<^R��H��������$U�[�23��m��^�xO�X���������^P������570��$����"�jl����LE.���9�2�@)�{&[���I��yy>��A��w���bh��RO�\�e�����d��Zl��O��5��I2.G�i��&#@����0��g����,_2f
>S��W��%�%
�#w���(������B�_�G�=���U�1��d=�$��V�rv�|��!\�2������'#!LY����\���5��g6$��@�B������=���m���m�����@��<�#� �nOn��e������,��Z�3���Q������m���~
�z�2Y����B�E8�<�C���\g)5oI��1����\�r�����5�k�Y
T�pa�e��yn�8���
qJ���-�x)�s!�k��F�c��Y��-"������T�����1�<Ln�V�u_-��A2��A2�c�����T��B��3���`����7x`������1{9�(h��
V��_��O�N��P�~w���"������>�y�4�����>fy���
����P����������#�b��5�.��?����wr�V^<���=���{��;I�htfg��`���k�,�

��/gl�phB;�V����U��82����g�{X������e��T`�e�Z3�=1d9n��"H��omL<�3�����8�,K�fi���{��e�f!��}<aQ��D,��'0��������.����R��0�u�E�|��bq�j��0.�9\�Y*'�����<�l��c���uu�2��#�!b�//r�X���%�8�l/bv��O��5����/8�f�2�Wz��.����k=w��L�1���P�(��-��p�*O`�X�V��0����Y�����&�KrM��|I����=���?�Y�	_E�h�l��0L��mw�/�	?��bk7,m�Jy,����-|qh��4�]�if��z6�y���1{�#��/�7Y!fa���6�=�������M��\7�2�|S2M���tfG�6d� ����7,��
�%��40B=���?�q��=T��\����E;���c�ySY%���AN-�`�J�+?��4z�W�����=�q
���k�&w��I��q5����~H�8�>j���E^�C��-��HQ6l*��}��8R��f�{����L�C��a�Ke����m�=�1�YG5��:��D2�Wy&���W1_�a������HG)r=���J_��D��'��rOfu�Y��\�������n��l�O��o8x�W�|B��p�!D��%[�y��C%��4���r�j ���m6��isX�����N=�~��q��,&����=�z?]V@�ajn�C��u��'�I�L����M�;G6����]x8%��J$���j�8�	H��U��n�r��7�q_	��=�}������=&�wL�wJW�<�;s�LL	�������x�w�[K[���	���`�����
>��%��U�����>��$�U��i��r|]�-/���$	g�p�<������j�����/�����[����T�.����
��_�>��
�\|�r��$�=4v%
H�m�Oq7>��(�$�����OFv��k�z���M7{�J9��J��=s�XO�����b�v�f���>rv�)�	W+M^�M#w��e�O���5 ��^p���jF -�&�M��+��-��6�a^#�9�,M�[u`��+��vo�X�"	���"	4<Y$%��
7,���k�H�����lX���=��:4G�:rU
&Sbb�I�����I�j��xt�l�
f	W�1�Z���IC8�%a��s�t-W�1�}��:eR��-��R��������(�^eE�8��sNM��5��9�f�44����{ }V�*J5��2{��c}��7#�pu���"lp�~�j��N�c0�
�R���?K�F��5N�/#F��Y~��T�h�o�:��l2�J�Kv���!,�#sp!k���Y�������-���T�D�S�d6��/n��1Y�s5���_�z�ma�x
]���pA�@`rN��
���b,;N����D��~������[]M��=����+��SeR�]��Z����*�����]�w=��������gd���c���}u�f�����0�w��hZ���"����uK�����+:
n���c�=nj�qQ�*�dLt�]�4_}E��&�|���Y���%E2�z<�\Lh����s�
�R��|���j�h���rth�R����
��o4�J�&�|�����m���V��)��4�$�&`Y��q�5�db��Q���z|_���<���]�hL�J��Q
���:67r�`v�����}3}�}�D��������������M3�6���X�Q���Hq�����}&yF�����Sa|���
�#�G����!���� ��HA�6\�ZO/s���0$
�p������-�5�"LFC0W��pc�\e��7W��a
�6;��s���0eu����:�_������]����<����g����S4�[�QE��A��-3�]�?|�KL4ae������Z1=���A��E��u�HZ�O���fF��z�s����5g�A��!���j�'��0�@���h��������������Xh6G����9�����M��8���3<���$���]F�D=l}��tb,�s��C��;�(�tm�Q������^�G�)H���<�0�:�c�\!��t#�4k���Dk��_4g���i7�=�	��k0V�����o��� ��n��\L4�D!����|J������
����T�)&�[�����X�
�c���I� �Mr3.l-rrA ���A�`���J���+�; EC���dRof/�9(�neY�tK������~K���?u
_)���sqH�^V�������v������m�f���Es�A��d3Z�/�6�Q8���n�6�����q
/&�m�����`0Bj�ST^Y3��b�
��!�#! J_�{\V�b��l��I��_�?�$�\m6��}!���(�my|�()���(��{[=�o��>Y����9s�3��#]���b��r�5TK��V~����i\��Mi�8w��M+�?��EW:���%P�� �rF�2��Y��H��^1h"f0���-V�L���3{��f�^�pi0l�an�!r�B`AvYx��"G�����������&�p�^+J�enEj�FT��1w~z��U��-	�]��������=d���T���z+���rU��uR�V��H�=
�7�RE����3��".��~�>fZ����0��v���h��������so�KI���HR��_8�N��.����F��yA8g��l!�_>j�G�v�1x`�]4jzx��,������;�e��6���V��R�t�v"��#}��Rh�z���v1�k�TrS[C=�������|��|)��^JK �EO�Z�#��{nwX|���������F��ck+fq5a3�k;
�%��xn�����Q|82I�Gi����%|��Jw	Y�I��7��/?�{?v�(�����1��y�����>.&({�i���j��#�3N���5l|���U�p���D��bO7O�����u1��r�
�c�^w[��M�`�����>���V��UK;�rD��I�T.��bF��:
g�N>�2�i%P���<�����?��? ��
endstream
endobj
27 0 obj
   12660
endobj
25 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
29 0 obj
<< /Type /ObjStm
   /Length 30 0 R
   /N 1
   /First 5
   /Filter /FlateDecode
>>
stream
x�3�P0������
endstream
endobj
30 0 obj
   17
endobj
33 0 obj
<< /Length 34 0 R
   /Filter /FlateDecode
>>
stream
x��}M�.������wc������v�A2���$K3K�e����_Ud�U�>s�@���d�CV��l��/��P/�r�	�K���/o}�����������{���oF���J�ee1%��K����������>��|������{��_����_�������������~���_���oJH�^J;a�{}������������^.~1��4���o�%EJ^������� �1/o����0�eE�����^?��7�����I	k�r��WNgi����V
�5���^�d��������fT����q>S�1
��G�Sz���QK��A��x���x�	�Hv��%KO}�1X��cN�5��t��Y�pRS2�LP�CJ)�������z�{��������N&E�c�m��t�?F�d�.Xab�VL�����[D�(cP�����:����47
=x�|��J��5�[�t�Xq��&�ORE���r�ugHB[O�@9u���8R�2�+sT�����:�_�{�m����-�V=<*�NY�BI���T�?�x�F"�2w��l�I!
:����tZ��<�N��r��K��4@�Q$��+:�H��]���@�7�%���)���a��]tR�Q
Ki4������p�qc�l�I�4i3}���b>��vl9[�.O~��W�g�^����r���E�/�����V;1' ���;Q���]��It��W2
���I���6�p!�j���� ���Q C������F�e�vl}'����N���	�%���f������%�'
z�$D������j*Et6����������'��=�g-��7J��{d�a��i������0w[S������j-������x�c��^7�������y������~��~p���"�
�H�� lx�~
$M��~.��	?Q�?�|v����>~�����L��S
�E<o��������~���Ti�e�p&=X-@d�	��f�7T	G������H����X-Lz`��������{�eO�T�@��lT�-N��5�v����U:�_���J����V�<N���p|� c��i�����I�3�I�)������;����8w0d����1F�����H�����JD���+5e�n��$�dE�U?�M�1~�[K�����T�AG��/�a\W�
�j�(��I����������Q�'	l����q�0���=D�6����I`6�97��L�vv��C��?��>���dfY�"[�}|5x�w�[r������p!�#�~�"AX�|:F �s#�����}UQ	��h��	��52�^�:�L�@��<
����n�I�B)C�%f���^$������3<����������R ;R^n����}����b��LS��-d
��������3�d����Bv���R
����,	�ig�nq����;�ir��vO-�	Q�GP�N8��'x���2�c���W����HN�	x���_8SRXIP/�3���+�l
��-�^H�S��oTg�x�blq0'���Y�lZ9��'��h r��4�k;���7%@\+\����E�\�i�d�Ak)���H��c�O_U��V(�NO�������B;�������\���@:����{�Y�f��]WLW�O������?���2���f_Im��%�����?l�9��=�;������1�E�q�=$0h���(w�1���H�Tl�7�w,���d�}]���	X��u�u�cA2�p����)d��s��J��U�4��}s�@��i���0��ZaI\5�
�X��1����+7T��Y��
V�2:<�����G�so}�|t���c���6	���fz�L�"4�QO�3�����/��s{�S^bK����r��.�l�vQP�
G�B1�>�34��8�h����mp�����w2{_	��F�]��A5X�TK;� ��z��h�t����G��2���7q$!&+�<�H�io���l
S����^�Y�a���+w�K#�4��An����`D���a%�Hh ���	�.������?Ff�3c���9tLWPO��A�;�*t���'�C@�fp������~�t��2 ���Q����?&��&�	��l��>�FP����������&�-��v28GP'��u;C���D�k5Z[?	�Bi�
���C���\!Q\����)
O���2��F'������F���b�R{�c\}�X��t����]#AKB�D���R�:�"�I����KY���������[����Z("�*�u��������R��J������d��|@,�r�����"@5�_�&�t��GVY��|��x�Q9A�z��h��W=wxh������O_�1�N�����D$R��I��;5\6j���S��# 7��q��n�z< �N�18���@~�VH�����H�!r��Y�3�<0F6=���8�+6x��k���i����qn�"��Cp����a��lR�LD�x�������
<I�q�<iB����Y�\���������H�Ra���AR��������?��px/C4�5�|��N�Z���d�>��<n���M��Yn�e`�oY��b%���@���C5EV��u�y���t_r~�M0F�h��; ����W��MA��(�:<?	�����?l���/�$L�1F2(�������.���pr�
���"�{��|`�"���8�J8O��lf�5#��o�}C��u�vu.W��Ug��]�#�� �r������vQ���F��43G~]GLWw�����0�����������h�2�����r�]���D����K���s��N9�_W �hd~��3�066%��P�^(��"]x]Ld����������sCC��Q �2|����	d0�+d�!
����*�d�����t�);9�����)��_C�;��n\I�7,PMQ�d/�{��8Q�C�}&��5�k�"(J3���4~�+���
��eOw�����@�#��=1�������Hd��c]
w/G�t{�>LG\\����7��!�H�P���(�<W���u��MA��X�l>����Ov��������Q�o�:�����RA�X�Q��@�������(P�CS���`�XI
5Vx�A g��)x?���E��7������<-��/���D�>:���
�[;d7t�����/��^%%9>�}n�{�������b����<w�(����n�����
���L�?R���>_d�A�����v�����s�st�{���3�����+��o���t���D���c�GoUiq�.,2=�b�}Ez�}�c�q� ����UV�}�]��=9����w��$|�����tGlT������������$t|^|�D2��	��PI7�+p ���!�{D��i!:!�H��P�,E�lr���n
��-��!���o\�����%�=�OTq}mn@���XR�d7��(�~���s��L
��5�)��x ]���G$p��%'An�	4FDO��ej@ �s�N��Q���D*	��T�|��S��.����>^�
�FQ�5Q���Ez��^$���� U�;�� ������bh�u�Z#\��7+h��cS\�Wm���f�b'�i���<%�d(
k���q71��Q�H�7LQM����Us�hJh#��@���������gM�Wm�uu���NW]�L��Y�.�O�8�'���u���(��U�K7��68�������Ce��s~;��v�9O��$t"�R�K_�6N������!�&��c�cNp���K�W��4������}t=�w�L��R$;A�B�u�1s|9����$�"d7�L8�<4%�'5�@�K=h4}��,u�� e��]����D�]�3���K�(��d2#��x�EZ��%*4�f��������H3�;��<$ghJ����Yv�Q���������,����|��$�GoX����`2�1�����yY����7r����
x�5���G�'���s����HZ(����N	�Wc:�2H6���`vH�Q+cSb����T	AHEV��'���, ���j�G���|G��"GC�}��c�u��T��m��9:i�8������h
���N�c�t�&�aKJw��xA}��\�J������������3y'g����o
a����s��pq_���>��O�)3}T��33$|],L������@z���G`o\��� q^XK�:�T0a4B�2��9
�<�.��c�S�w#�#e;���efH�?���i�F>(��x c���9���b�;]u����u	�����^����_�g2����x��?�����}�������������K���_?�����)�>����/���i/���?����_��0Qf/�?�����D��7���7V��.���#P'��8lxr�:����O���v#�=8����&D��#T����O�P��i�����M#/��59�e��''���|����:a���sZ��=y0��*�u5�+���X�W��ON��
e_#O�����Id+�E"<�VI�����aB�'��-}F+T�#�=9#��6L����LF$�Gd{rFF���O�5m|x}(�
�p��c�Ja������_mD��d��G�~uI���Gg��"����������k���|jO���A;#��s����io{t�tl}����?���5�����������G7�^�+D&l}t�&%������:��Ln{td7~��i��������4-������B�i��G�~�+�����n`�H�����XcE�����n`��f~���,����4������M���}��3�Y������Z�����1�8�����~�����m��7_A?�Z��5a������Qh5����5!��o����Si^���q����8�mt�p�4�[7c��s�J'��������Aofl}t�W��������]��=1:���������l<��3��2����z��,�����R85i����	�������������n`S�d��������f��=9��|�A���G�^s�}����i�p3�����0a��=:cC�O��?:c�NOs�?:c��N��?��
B��>:bsx=�ib�GGY�MiZ�Q�x�o?�����Q��S�����r���$��(�S>���_�~�����/�����w���/!����6}���?�� �W:�������w�I��%����TQ~��*�w����<�u�`6�2^����Z��������L�K�������tzW�	��QD��X���^T!�`2<�����]E$���2�)�����)�}0.�o�~����d�������p��A������+���M�X�|q���\������V��^�h�-��t~� ���M�=���>B���V��Q��X�N��>��l������,�w��m�(�D�J���$�>�[c;@��00��d��|W���(G�%�]J* B��<�"������t��7���r�>"��U��I�����?��%�|���_����gGy!m��6�����=;V��N~o�`�gGd��q<�ggHP"up�V�.�S.���W����*R��fLR��%�[�M�]�G;��M9H|!����d�G{
$�1]��(L�@�DEh ���**���*0�'�����5
�����@9�9�k Cj ��7;
DJ����QB�Os�a����{�B5bc�{��+a�[Q��9�w�����J!$PX��5���H�)���4 ��[:��z��Y�&�!����e�p:���}l����V�.�Em,��
@O�:�.���������������:Y�;�� �D6_W#j��{�����i:3���[�MR�:�j��U��	�����Ym���q�O`B�j�\���><�|�������
��M�
��2�F���5B����������
�:.k*��h�|OFG���W���E�u^:`�)V��=y�5M����0�V�|b@[��b+��F������)��4� p�v�v?g�'�i=��ZaTH��mE�O9�����3��$�yL��1����������h��7�������t7t������m�v[���v���fN$#��!j-��ww6]�x��5����*<�K�� :�Q�|sB]��o�TKa�	Q�$�SSz8���4��{�3����T��|����M��9�$�]\1�J�GP~h���OE�~���i��:1^����M��;�S�N��Y����Z�6�q6��O����I��6�%�j��XUM���" tU���(�(U���w�b8�]S~���b���F�XH��
^uM ���7�)����v����'S�g��L���G��������T�0�X�`���`i�_����[�~Wg=�176��`�Hn�Q���z�1t'�
�����k�D��$���i�pC�����wZ���4�/��2*g<�k��L�%x��p���[�e��J),ZTCM�$�D�=�G�:�����	����>L�*���@�<<�9l�g��1&`�x�c��r�q]���t?F~���#Z=��!j��1AX7����Q4v���P��i���g��z�aadm7����,adA�G+bB-�����^�w;�w��]vV����	%���DD;o��8�n_�`_��	5���w7����M]�@��T4��i�S���Cp���T3�H��d���p+iO�9A�Hg(]�w�R�N��ds��5r���rqD����+�s�w�j,�K�":�3�F	�D�b&o�'��/�����u�v(o�f����q|��);���&U
��w��A)k2����g�X��PuR�pMG'+���Y��m�F�����b��l�F�ui����s����'���5�E]�������6�#�����_�����S�H�����rQDB@�23|G��\�V@�����$\��.���s���j����Z�'2�fr�����k�Q�;0��QoL�2�c�I%r�5/v���s%�B���1���s5�%���p��<��k����!p��&-��:��&��0�\�G���7�������,�2Ik��g~�����L�.c��=����
�%��);��=@*��:+��m0����Y�w���;��.3�H�������T�
�^�uG����h�@iSMe���Mr/�X�i]�6v��	��N�V�f[�rm���|�����^����Zd��u����}W���$��4���hZk�w��V����[���l�7������}���d��l��>hPC�pN��`n��������P�g��������h/d�Ib���$1�	
�L���q�.W�(�}��Q�@������R;���!0��E����e��A�5��rt�b��D��S� D���Xf�$$<$���'��mf^WU�8CQ�	3��W�C6�����l���W��^^*k�
�_�Gc��]c[�Ki����v����>Y��	��]���Z�r��]w	c�\�D)�K��D������HdY�Ik�L��
�fCq�XgDg�����-�����Uucsp�b!b��c@����2�A���2������m����a#+iK����f[�x+g��9�5�m1;������6�������F��������E%��h�T���wK�
F��s:a��w�	fW�t�6f�m�|��7��EX0q~�$���R�_�����"yGH^iQ������'$ ��jR���M�d��V��F������~I��S^�e��_W����>���%n(5�u��V_1S��l#x���X=�	���4}�$�@;��o�A���_��=����4/{]D|�omMH_3h����F�	���^�g��G0��������d������T���d�h����v�����|-c�d��.G�iqkm������)L�����H#�����.0��U~ns�4�r`#�

�d�"�1*���*�o�-�I�;!��U�3�|k��� �1���1�B��L�BW��**NF��8&�r9�aG�H�>Eo'��nE�I�+���I�S0n��KN�C<�C<D�\`�]�����jIp]
�J��CL��lkk�<�l	��4}�(��=�l��&�.���#"��a���6%���/�%��p��!������U������tZ*�C}
�N������i�����>�����~�P�%���v�6noQL���S�t��������<P��B����7��~pDV/�������aY��4u��'��|��t����(u8pI�^�����Q.�J�)-�|[d�6���j��:��(uLV����X7�P�gD�WDl���*�W���@�*���|����8�f^5��&qb	6�z�u4�t^��XR��~�u4K�i����l)�����C�8T��g�4���C���IO��t���j�[���Da\$-��e+j���R+i9�VNzP�F�'�iS����I]=�C�[;vKr�b� �]V1���jj�/~�
���Pa�b��g���P�]�n��,���{�R�V��n����}��s^�
�L�������j������=8�������=�j�]���|�������d�{��W�8U���,�x�*�.Ze)�@������h���u����"V������j"�7�m��^�)w�V��M��	�M����������A�����T����'�v���}�~�'�%n�*�I�:�EQ�u�����{��&$�h�����/�Bok�-�y����h�M�ec�a��)�����B�1��f����l/� ��Ge�6K��C�1s��0	����t���a�����]�a�k{:X�gd����l���O�e��+���k��L�!�k��
���.���F-�]��sd~r3	�n/������������JKFW��:`��U��-�T�E(�)��G��S����Ia�e���9���mj�b��t������I�O��[���FX
V��58K���u�tZP�]�T����d�h.���`'�f�����j��Fd�,�6�rW��@Z������s�^t�{����������o�����������������xQ��������F��sR����)B�*���a-���_��s�k��AG.��|�LGx����d���JDj������ ��!�B���Uj���m��n�$�d~�CYt��Z����<��d������NM����Un��~T�7f���pp@L-o��x�l^J�T�����������������$���z��g��s���2��������q��X�p�$��\��%}��c�
k�/TI�m��k���>�{]���� n9��y�R�����
2��n�;���u(�.	cK�[��$k6�,T�D�7�'�G�;8&�B]���jg����m=\[%<���Km&%����K��~u��P_�Il��j�0f�������*�����U��|�?�����)
�|��j���<���������O��j��"Y�^�W�q��>���t���|��&�p����Mj@�����}��p�8)G�)h�V�����1���8���%���\(�twDV����F��qPi/r����� i�wHZ��������nm������.�9����W��I�8<���#��	l�Cy�������~�����&%����(#P�z��~���$x/n�g�S������#+4���<��{E7���.>���W�#
�����p-���|6�����������y����j�>�=k�K%�������U�����@\i����!��d����Y�yG����8�����4�M�O���I9�E���!�;���Uc�Ej����sMM`L���g��8�k5�UM	�v�gA�f��!����4k��M��# O�
	I���~@p�����J����O��&jJ�JZ��XB�]:p���K
��}K%�S���f�sb>ey�����]�$��Jq�.��`J����6����n7Q3���>�������FW#��K����Q�����5@3�Ik5X����)L2Y�����5��Q��8>��������D�&_1/��0����H�����O,q}���r��#X;rF{�����7\�m�#P��+��
s��7���D�r��r�����z�P�%�f#��
]by83"����YV{0��G7�@��Q��:�����:�U���n����{�0P��
��gu@Wd�P��OT��<���_8)������l�#.�TQN�p�����PU�m�`I^\	��y5<4�R�~��>����j\���q>ps��vT��E"���������P��u��K-=��M\��=�NT��������E�����L�7
�z����lQT���Y�<^:��$��� ���_)�S
��8NrN��l�V�������	.8g��3_x�_�:=/���1k��N�X�Oi+8��B0��*�9�!�h�K�2�r�X�q(�N���&?�DDf��Z����A��Q�%]k(;�����X��}CpO[�K�H�)��T
�0|��ze���	�{�E�%GV��HE�TW/���r�V�[�v��}���]�5�H\�`U����,b��0�h-����;�h�-r��W��{Z�&�f���x����c;��������C�Z����9���Gp�=W�J�Io�e<��g�R�H&Rj�����%���0j���-N�����O"���&�Dz������,3��	��$��c�3��i8��$�
&�&�x�3_�e{B��-�������c��=��]���0����%�j8���N�K���@~�y�^�sI<����[���;��[�C�`^���o~���Gg/���q8l��_��z��5$"�	s�y �f�q��?
�<����y,������N�C5�����*M��������lI��P��#����:JE��c9������mz���0������?6);�)������kZ;_h����p���"Pv�/�W�bB��+���>�,�0~6�R+�����m�����n� �������;tMw�9�|5;z�1!8�ns����Qg2��5��� �����{�O�K<�g!�dN�c��YSk������Ue��/=,K\u��TF:� �"Ee���Ae\�����|��������
9
|^�Os���]`�������UL��XF�,�O����\d�����������B>y��|c
O��>�@�������z���w�t�YB�E�l�r�~�b')����ss��!s��3{[!��.~���G���/e�;H�N)]T�R��I@Q���&
p�r���~f��~�h����2�`��3���`q@��|>'��o?��]���lo��r��b%e�R'q�t8���^��vV>=�I���[����$7w�[jX�a��d�����K�&r����
vU�R���T.���������
O���������1����fV���w���6���E��E��v]t�� �pV%��i��RAl[fyDW%����������*�|y�1GC=�Q_�va�29��o+`I|q,��H"�����k�w����Y%�46����7���CU�:�U����2I��c{t�Ca,D�&)Sh�5N��h���!I��]��.K<�"�$�aY|z�7�I�������G:�$e�%$UVI�`�$e6�j���,�b��|8	z������U��g�w�0����td��9C�KSY/!�������x���1�,�9?B/��{��73 	5�?�w��u����)�|�����b	
endstream
endobj
34 0 obj
   11941
endobj
32 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
36 0 obj
<< /Type /ObjStm
   /Length 37 0 R
   /N 1
   /First 5
   /Filter /FlateDecode
>>
stream
x�36U0�������
endstream
endobj
37 0 obj
   17
endobj
40 0 obj
<< /Length 41 0 R
   /Filter /FlateDecode
>>
stream
x��}K�67�����wSp������mh
�g�������]���k
��JI�PJR�6^�8���2)��R��K���K�\rB��z����~������?�$_�������RT������"���}y)�4����_?������/������?��O��7���z����f>��o�����~���%�v�/��0�����������7�����\�f>�����H������N��o���y����yy+|�/m��/+��������P��On���V�-��N�r7����0)�d3�+'��y+����(��@E�QK���:��&8�#)R����G��(|d(E��+��6(-RGm�x�L�z��[�l��&#�a��je�z��cN��k`������`V�n��R-�7�
�4^�?0o�!E���	�AMM��
5�����>���kbgp7��-��T��a�&h��b���Z��pp���1FZL����5�x��"�M���N&�����LB[�uT��E%Et29=�J��n��4e�7��F�)�~?z��`.�q5W��$��I��`���'G��/�
�J�kRX��>N��@�[��)�����t���1����N��+��Q��������>�z�_� ��|k�:���c�T3pN��q
>-Y�%�G�����L��#S�;��dDTh�bb��z#�����n���0��J
�����Eg����z�pE�;�_�_X'�Hu2��+
��W��+f�N'-�(c��P����wqm�e���:j)i�3j�����G��^c��wQWR�������h���?�{l���aE�f3(�NQ&��z�e�
�w��;��#�+�`R�N��s�Zl�;^��N��-�>Hw9Ab��g�BVua`��s{C�g^��/�Bw���7�&|G��v�P����U_4C�i��������:z����c�5F�E5P����\�:Xm�C	��:yoo?`���.Y�YF6z=�d,j-�'&���������[4N�����'�7][���u�51�bH�(��R�� V��*���hC5Z�H���^���

>�j	C%4����S�~�@>jIpO�V�g���@��������{��(������Yq0���&��w���`�0����\�3����� ��(f}��N7��F�������� �A���%����v�(��#���nDl�8�`�#st�>��3���u������J&j~���
�1�������x �5�Mh ?!�3���:�/X3�P:S�E���n�}41�m���Y?�M;S��m�����|�uN�w����^+� >��b�JDM:wa#�J��mW'*L�;]>���1���"c�b}#����r3����w"���Z�����i���0f�c��l�4�@�� V�i|�c�w���������7V \����dmGkM��6@����9% i��������F�dR�
^������~`�N�},BU� ���d�9��?�����A^���JhG)��_W��)����
����,�/L'�c@@*?9�q������*h�9���Jn�tR���K��Ew��O�������A��f������S�����������J��2��%.`�������#��Y��JND� ��sFy����;����v��@����d ��F&\����>>��4��8*\�7.���v,tNE#�`,����:�w#Qz]h���d���;,�>eq����Y��)�x��ac6�����AXy�o��P���r�b�28�����'d��D��odp/�w�����t_�'���)�iL4|A	��SB���H�o��I@���l���]�zkd��&�b��
(�R(M�1��x'�l
��-d�MrN#�O���'��.n+r��s�D������}C��:8xi�D4D��5J���"k�_����h��U��}��?�����O1�t� ��L�c�OdJ%cL2��2'�������`�1E����Jq������1�w�#�W�$	������A����;�����2m~�]��#F*G!�N_������Za�TI4�x�:����Q-�v��i?`�$�?F�)W3�82���Z�l�EL�B}s0@�!;��y�l�@��i���E�
E%�D���K�)f�����1g54VY�>�m7p2��n�@+���W�/�e\%��)3z�2���y�������x!Zv�
��{��D���u��|A{#"�-��D��%�6N����*���Q��}$}v�����6d������w;��@ �������2����/s;}T�M<��M������c�2��\��F���b�a�n�����*���s��H��h���F+�?����$���BK����������'$p��QXE�9:�)��t��i�fI��e����;��e�a'@g4�����[�F$�;$������i��n�;��Y��NQJy��n��0#���uuD��|0=p����{#�� �nQ1��U�Y��!�����{B�?�n�������
x���d_�� �]����9u�(������8�wt�M�*��>:�)q��������c���������uz�����j�Y�c<~��������4��,�w����slP��jz���]���t9�?r�Zt9�������rPZX�*Y/��,���Of>3T6�������8�-���t����H�9(����X��	�t����u�+�Y��p���R������\�e���P��Y�f�0Y1~s�`��L���H�b�09��O�rP^�$3�s�'�L�����-
���|Q�B�c�g'*�����TF(L~ �j�����r�j�rx���F*�?Y�$~��a;�}�����fM	S���N�A�[�d��
���Y�r]q��
X��v9S-�xQ�uH�}#�R�&|���D������s74F8�	#$~g~m�����G`w�K�+�i��j"��D�qx.Whe�G�2xN��~�����)+}���~����)���| �u��$��Ob�rx&�
h��'�b�n��B�]R�V/�!����'d�L����]����
�� ���7|�V$3�|UFH�e�0���-��R�|1��I��K*��o���V$3T����t]�H�2�E"�"�����r���NDG�f�rkd4,�/E���z`���A�1��S�>�"��i"�A���L`��<
x��������x�B���v��
������y;���p�n���F�w;$����e�5�� ����4�NkGoYe�K�Q���QY���g~��!����H��p�[��������q��r�Z�H|'�bv�0�9)�9���5%<-���x�
�'l7�&u��2��9��g~�d��t��2�;����������F)�kg��~��D��:/,�	;��|��;QIDG�-1Ge�q,a��4�M`��sQ	 ��>�o�o	fI��������b��Ni����������i�nN���v|m�%K���K"�� ��S
<Kcb�G����U�i��j���Z����(����d�����G1�������zaU:�}a0
��
lOG���\�<~s?����M������
e���N$��I��*�-h�����&�U���o���%!���;a��DO��A�,������@�����N-��b���=�X�O����:��:�����
��
�n�+���� �%�smpEP>K�����()0���d����o��|�c�w����F����h��-�,������o	|��y�[��u������_��94������7�t@�G6X����].����-��uU�L�*F�]o����t���k��0S���H�����H����
��#��	��|��R�H�&����rC<(����vh���?���;mJ��	�B9"�� y���6��SC����m��g0jJx
�aL'�&lo�����A�$��$D�������r��h��=Qx_	p�������s.z	�A���Qx_d��
���B	�t���M��8z��N��L�/�$h���'��	�8![l���8���.��;�3���� Q�����T�=�N���q���c����v��S�tp@��
����������&��X[.�z1y�"[d���)?�b:Q���n�oA�w3�N/���1�`��cd6�7;�y��;]�$=���=Im��@������M�.�8����fW@��v����d!��Jy`�j��O�X��jz�����N���s��������pR]��N�9�V>��o|�McL��	��I}h�>�=H��>�8�	�2�I�LPZQ��/��p���,C*H��#���:�'$�����u|�pe��3��
���J)�a[e;��U0����9�Z%w��3���A���v�u�k��u:i���D7�Q-6S4����w���!<�P�.�
�:��6x�I+a$��I�g�@)t��.�t�E]w��<&����Y����Wcq��7���u�T�y�p���2=��?����Jc_��!_�P��}���K
�������������~�����������V_������������_���M������7��<X�7���7f��.�K�[�N�k<lx��:��-mO���vY��Z��^���'[�����dh^t$�)�0-����	�OvP�}4�"(<�A�O�wd:��P4�W�9�]k�����W���DN7��O�%xu[\�d'�c(�y��]�+$a��~E"<�����IH0�>�"�I}�'{d�����[_����RF�1�=���T*�[����Fl}��� �v�>�cm���d�tZ�4�Zq�O:����[��
^dkc��=6z��&�������.
����DR��������z����m���e36���h��Rh9,��h�5V�8���h���jI�
l��I7��>�v�`3�Y��8��h��K�&��d��F$=�m}t��"D7b��=6D1"����0�*h��}�(�{����Dn}�E^7v�a�����prP���VR�Z@���-����p!0�>�c�iT���k����i��^89��>�����,4�����:/��\��^	������2�ly��'�V}{��WQ
��1�G�~�&�c�����������l�'�[m������'[I���Fh}��U[��U�`��i�^O�HE�����=���8��h���97���h���B�a��G{lN]I����l��c���3#��8��h�o���AW�G{l�B�a��G[lv�'7��m�7;�����}��g�����7����3�� �����������+�+�O�����:�Cx}�����|�/�R���|����������������O*���>���?���5��/��tZ�����>UR������@ka��/k��`]���
"	tN�*����L�*7J�cM�!&�?��z��mQ�o_���i���+����J�?�6I~Z������JJh�K:��)�6�������J��d�����>���BF��A�k��M����U���()�&����]4k)'#�Iup��}-��SE��:������q|��� ����%8S�?��al�v���(�V"x-�I�y�]�VZ�u�����~-jUQ��;8:�Tund��VJ(XX�tQJ�
���Dw����cS&w�����\s6x�'1�o_.����G"����k�[kc~�X�T"h���1��I��|jQ�,@��I������en�������4�����F�X����)�:y�(��>�+� ��&����2T�����6_J;��Mz�^~��)�O',�����g�N���mZ �;"FH���h����2������6siu#�<����^W*@7��p�1���3��L�0�m���O3���JD@����>������-����P�����{��p�+���W*D4�MH^b�zN_r+��0���t4�^M\0Z:���Z}��X�h�W�<RJ6�
���Z�?��f�>�*���:~�&��@����H���S4���Y�5��I�:6Akjv��K����	�����8��}i4.��]�@�;��.��D&�nA�]cKZ�}1���c�=4om$�v����l��j�%�� A

L`�HPB�Y	����HrU�Ac��u�i�x�����wV�H*wE�72�]�Em����qf���K~�HJ�6ai�l?�HR�!��^����E6I�j�4�Z����0�rv����Q��d{vI`�
-XAR�6�����R�U�Tqe�qI��$dM���J��jf!����,����E?Lx�t�z)7������h�|����RHm��h��ikt�T9�#t8�&cUB�@�L�
���>�h� ��&��r�)��������k*�#v�pi����f����Lt��^�_��K��������G%�0��]����6��`&/)%�x�L*������Qgl����OomF�WzBF!���	j"�-SFa
��h{��.����m3d����VF���ud��GV!}����n��e�Q&+��j����v)M�[9Z8m��_����rN���t�pj��I'�S^�y��}�v����,�j������2���>P�
d����0�/�o��n*�����4�9�gI��kU�l�2��!/Y����,	�E��A�����,�@��`b=�Mf7C��?Y���g^��0������l�,lGbz'�\q!
��J����x=�C9z���7���������;�N�*4+b�����C�F��!5��� ��:��n�,�K�w�~���k�D����>�ah�jD�&�q�B�`]�>����������������>��@������E�3ErA�v�>���_?��������r�����	��3���A4'�]��_���)llv��N>�43"
�;����q��x���c5|�*����p��)Xk��G�iSu7U��t�h=:C�0*�H1���0��2v���MU%!���4q�nS��&���amv�*]����������=Al��i�8=��������9��m=!K+$�iQ��~�V���G�v�
���z�]b����+t`b�#Oj|��_�F�vS������R&��W}���S>�b�&���'�l���"O�����/�k{#b��������QHp�J�]x��6����7���7_�#�
�j�*[+e���k��9z�M�.��8�S��5bf�
������T��Q�G��
�h�,���T������p��m�����~���NB�����n���#��YZ$�b������.��h?��'�k�X��.o��%m����f�m��+a��H�����=N(��!���K"\g�b�GX�LH1��67��d�G��RL:;���:��@HB�MHq�Y}z������S�tI������m^f��~�:K]�o�U������j��eb2��2WrT�f4�m4k�f�/t(!��=�C�iwE�p�yoC�9��t���,����=:�L>�7p$�����b��
;�<�|�g��}4�i�Y�V�]L��l��R�CNd2��B9�W�&e4�b��2��b�6�YC��h����%��(�Du�V6�Q@w�0���%B�0���-��7�[��vw���|�g��.�������'O��-��
6�dico��7�Y�!�lq��}��6d��:��v~1���
5����������m�����%���OF8�X�P���^�n�~���
2���	��8*-!?�u�2��(�� <��h�C_�$|����
�&_Fy`#_���S~d��N������F�*L������
��AI�����5G�F��������h>��S[��+s� ���^��GNBw��]�������[�'�����A�1&�B^85�:�E����\�aO�5|v�N
��Zf��MT�L������~�[�����lL�Q�Ib�a���!^��h9V@��4�AA��a�m�
�Ft�8��zbd1H����b&��mS~
�t�U��6����J��`�-h�0����e$Hw����ea��e�QS'$.,�y�w7��*��%��+���nQ�kp4�x�Kv����[(}��cA;�&|��T�o�:��C��H�Y:���t����5�/-J�E���g[���Hm�fwi�:XB7G	N^���"��p��6��=]�{y/��i��@��f��Dp����i�,k�G��K�Y6b���dd�}?������!
u����Imd;�������T�b�^�
{E)K�F����9A&D���:��(� I��LxU>e�[|����i���3�Z(�%B)-J��}���Bq�Q�u@BB\���}�#�Lo�Y�b�������'�==����s�A1_�4,/	��%�V�����`�R�k���#e��&Jv��=x�����"7��4K������f`LV�4�Pj�<��J�����K�����'RM|�	�T��Q�����2�aH5i�]W@�VtU���>��kZ��/~A����'�������vd�_�v�+�h�g���!��>[s[2�!�,�|�zP���d�_�i����Pp��W
���Pk��?�8T.2`G=�$ �Fx��.'�Yj���8�lG�P2	{!�Rq������C��:K
"�d�^*��AV�_[���:Gm�D��0����j���������\�%�*�����O���#�>��"S�����Y��m0�M��F��ZJ�-�!Sy��p�j��3X��(���o�_�C�7�OQ%�_����U�L��������Wp5���U���g Ar�Y^L����y��WtD6`I7Y�.�������C�F)�]�T��8�rrSI�iS73�7�mHz���k����������jn'�e�
 =�w�N����m���f��aJP(�-��`�A��n�-�)�~��q�&�F
%�����Sr�&Sc�zK�v�Y"��z����&Y�A���}U��|�&L��KB���y)u�Z��u(������X"�5�#�q����hP�!���G�b"<]����c(��'f�n�����^;UI:+b�ME@������������0e-�kqyYe-���3�W-1�{�T��������
+��b�1�>F"bfWn�m}91����y���������b3H��R�b
�[w��7[���sY���;.
�*m��hh0vU���"��c�;X�y���fU�(J�rwx���4x9����O���%d���Eo_EM����M@x9|�h$��f��c�-��+��g��:���bq�P�������HEodOw���L,��A7^�uv0q�U���n������7@���n����� O��C��d�WUx��ma8�� ��/��KAP2
*B��x:��
�1�2�T����d���T������Lzbn�����gZ���p25�6�����m%������!��&^������=4h��{w5o����fz�K�;H�4���9���K�U��a{����Biq�d�z�:���B��>]�E�14��r%M������H�f4�%�)�4 .j��
�������kS�����	�
��.�DaIg�+�J��8��MS�(�y�i��2�4?2e����%�
J�pf���B��a�����aIrTP2����" TP��HZ��>(�K
��=sD���~,�M�����=���Dh�*�$����Vx6'��r;�-&iq��TI�<:�_���bT��� OM�^�F�]^��2����8pj�<�Daei���D��;���I^�fJUX�e��9�pC�r%�������4�O��P���=3#�=���@�'C7��u/�=�f�k���6���bq?B��D[U�!��!�J
��|�ry����1J��35����~��(D��[Q������=>�-t��^r�TK�IBR�����=
HJ���("�Z�����4q����&������d��y������~�i��F>Pib1�kX��^��@������_r��^*I���U����lC�)��mt�C�#�9�L4"�
���Pe
������9��uP\����J��@��.@�Xe�V���P9�	�Xer�������-Q�2�r}%�J`6�6�*��kICu���n{�o���he��R/����(�F��p���k"�"Z�"*��Oo���� Z9� ?)^U�y�6��\:
���Tw�bk�|�es��N��sH���;�1`Y�
���^
�����V!K�~G����{���J���8��9Qx�w\���y,+[��!�\��H���_��Kk����J�CC��L��]�����R��
y+p9Or_����3���t]�L���\�Y��j?:�&�Q{��d	e�}(h�k�>��"tz�\N������LV8�������eJ5wS���Pr�a�������F/�a*�<��t������1��O��K���-)�U����^�of�����]�}��|!�"�2����(#�\!��:/�Z�_�0G/��sJPF�B�Q�����%9AWz����T�k�w"�����g)����/����a^zR���tiY*�,��FA��y�SZ��5�Z�8����W�N?3X��| ������f�X�K�H���0��t���y�Xr$�5YL	�*"yo�8)��<�$^_O��)e
f������K�SB�~�.�7��'a�+�^�!�0�3����F>G��'�x&J^��&9���MH�Kv~C@3�1���PP��k	h�b���r��j|y.�S������H���R�!(�Q�C=����pr����5��k�|������������"��e���l�3Y�'x�m�{{y�6_!4��*yn#��d�
%jW���-Qeh�5�R�.���pPj �.���^+b�Z���5;�O���wW���v�b��G.'\����{=��������
�v������'~�'*O]�.x����;n����!� 2�Q��$�`~cp$s*z�H���f��{�"j��>�P:����
-[`'��/�hS<$.�K5;t�y���#�Fd��J-�pb�0�K�����]QM{}lo������j��>��{��M5h��k������(�MqK�b�P
�KLS�d�;	.��	��x�a��|���J�_��g(�����D�
q������2x=�����8�eK��]� n��O2,.�+RT=?,�=���[��������B|`�Y7�^�:���^���$�w0�A�����b�-a=�z�qA�Ps
}��QG(d�/������R8�:B��8��n���(���h�c V�o���X����-Vc��8:��o�
��@o�Z��%:�b���UmF�p%N����8�I�T3��dY[���4����$C*p��v���qN���vk}{��WT^������l��v�
s.��N"!v�=�2} �b0���YNM����}�I!��p������_
��_�z���\�z� ����~(?��i�
�8}����n�Z+����_e���%�O�kE���Q�i�����hrS�����?�<�Oz����h���'���3����5����Q���|��SP_�2�����Z�kWf\��]%J}_���v���J�N�R�q������$V��C!$>&t��cJRG��z?E)?���/EJ�7/�5���
��.P��>���P��eS��D�����px�^����0U������kQ�Un�;����-�T"0q���+���v@8�X����#��6��QZ�������|���������)?~[obP�<>2{���\�R�s��.wK~eF����i���b=����<S�k����_>�?�$�J
endstream
endobj
41 0 obj
   12055
endobj
39 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
43 0 obj
<< /Type /ObjStm
   /Length 44 0 R
   /N 1
   /First 5
   /Filter /FlateDecode
>>
stream
x�31R0�������
endstream
endobj
44 0 obj
   17
endobj
47 0 obj
<< /Length 48 0 R
   /Filter /FlateDecode
>>
stream
x��}K�6������w�p��#�~�64����fa�(�vu�=�z����B)E(�u
��SYz^)��B��(�K���K���%'tp.���?��C�����^��A�~���?�0"�U� �/+��)�`_^� �������_��K~��z}���o>��������������|������������?������v�D������|������~�r���T���|�P����~z9�2����A��F���>��������KJX��^9�eX)��t)��I��)Z�R��[|��V��h��p����Q���k��!���"�y���ma0Ay`����}N��jz1i�d\g����Q4��0F�~����.��Sib��������|4f�����8������:C�
���6�?C�K�qB��T�
���t*��A'��n;	~X=���'$��'�B���
��W����e���+��������-P��'���\J)��F����Qk0s�
%i�N2G��p:Yis�&i���k�&��^/�1�Dm�*�	���t1$��;����;L�!�W��V�e�D���6,�W|�,E�:�������{��:�Q$��l�(C�%0C5��VL `��[�l�7�b#`��/\��3 mf(uiN��[����Q����!�tg�����r���D���D���0?�w�!t.p���\�cS��E�F��u�����h'(������O����{h�������:
\1�,r���p��F��LPz����yh�%�pF��Z�xQXt�N�b�R�uz�e������_�xgh���w_rP�I{��Us�c���m��Ft���D7T����P��R$��O3a�h[{,�C'�E�)RFh����	�r?a;��
�%�w�'~����v��W/�wW������
8c�KfJE�����n�"9��$���������Y|4eE�W�Z$OpR����9<����J��We��'��������z�L�to���Z�����������%��c��C0r�hB�<�q�(`{X����;��T x�����@���@TK4��K�#���c����g�@{��	�����q�~(x��d�!z~�����(��QSJ��0h��ja�qY8�@�O�
������-�v���{2oE�fxU�c�(�'�1���g�>��LN����J.��l.e[�o�l�q#g��@��B�����(��	�9U1�����q��c����+�({#�����:��2�P`R���n~��N/��i�<��Wq>%#��'��y:����1-H��O
���n�/V�.�A$��B��V��6��d���<�Q������$���be�i�/=��8���=cxrJ:�7H��(���x s�e�YT��I8�_�V�ov<�C����� ��������:x=��M����rx�Fwvr=�
O��9�J�"�W�	D�o�xm����(�����'�����M�`;���0����x�JhG��u��$��a��I*&���2]���U9�����u��qo���L�j�a1��*y:6������d�9�H5�����I]5d�C/��A ��"�W�C-����(C4sD2e��r�S�Fc[(���`:����A��������o��I|
�����av���=Dr^,��i���a�u��K1�29
�_z#�B�)�'���{�E��N0���m�U�G���1�!��Xr6X��2P����_R�����
@��KN����ztY������e����F�9��:���\���,p��WE��{
�������4~���J"��d���I�{'�1����';�7���e�b���.�6��<i=���?ILY�������s�c���%H��h �98C�����u�y��J�3bB����R�����uu���u�D�������d_�/+RxPY2�����tPA�a���]c7�9$����FO���&�D4D���mq�����������P�c�m�7;�!�qI0}1��4V�D$>�=e,�S:�N��I6���	;���M�EE��z��9�d��j)��(�J��Q-��P���|'�y��A��
KlE��	����p�� [f$w�bz$���^������{=
�����S�}PE�7\�]�de*�H:�r�sY9L��;]mtS6�F�#������ ���I;&�y@fw �R�|�.�� ]��$U���+2>���12�c�<	H���J$��EAEz�3�����K�j9�GrG-�#���{�6�����oD���7$����c����ZW��027@�� �a��������&
O'7�1Fx]d�!�H�j�WoY�;���3�� �
������<H��4����t�9V��H���"!�|Ea�~�o�<�7��F�O&\x��&�Q0�yC���>W��5�v4��W��������C�p�)��P�b�-ez�WC�������-+����*
��2������t���(�Q���1�@��*";�W����O��@	�]����J��`S����a�4�<�9�<�����O;<-��$�
:)�i�a���q�$M���G�)a�q�a���Eo��\F�������j���wT�9�(�+��pIjD���a�M��
c���5�� ���o�������l�l+N�>�_�
h�!������=&�!���I����7�E���OM�iV[�2y[����S����}��K/��T�z2U8�����8�O���&]�aT:<��n�H�\hd������i;����Y"����r�n������a�^�#�<������Ge��[��t�A�����S��o.��X G
\�����
����Pt�H+��d����0�����4JDK��4����&'�x"[��(9������O�wJp��a���!r/�|�*Yx��&���K�l:���-��y ������X�����d�����0��z ����7���r%;Cl������QL_���P�
i�����V���]>���&��Hof�h<{ 6i��:]�I^:;-4��z���bJ�����$0�dI:�-	����]Wwe'�~�K���i3�HX��D$�?yF��o|�c5��A|'w��v�^(��X��Z�,Ax�����vt���QPS�zx*��/O�0���_dS�6�cl%�	H�����O['b�M9��� �.������#��n�dQM+��P8�T�����s�����ax(��\0_���b����]uJ�������2����i��p���(^D0a�EDw���l��/z�����!���g�
��:��&Ja5����e�9��7��8|�Zyv�l�7
.i�����b�2"#~�������$f�����N���r��1d���t�����t�u��"��x���-������=I�p��%tr�;N)'5��9���3��SM�W�7%���x�?���dN����i� ��-^l�pq����
 �����\p�D$�Il4�^���8� {�b��D�I�`��H#xd���tPXiEP��������������|JI'����#t{>���{�����?x�&�?�IP���0d�������#�����1E����q0��t�R�����d(+,�&� ���	_Y�Z)�HnDP�#JP��M�
������;�8�N���Zl$K7�fa
�����}������a#�z����X9�G�V(����d����@�_\�\W!Z#������U������|\O�
06������Z��=�@�0M�����}-dS)dN�O%���/�-��U#����,��f6�0�7��DgQE����>	<H��;^���������beQ�Y&�������������jc��;@�5��'w�v�H ������/��+_�7�v�!�p"<�8�����Yi`2�R�Ea���`�j1H�Z����u��x��_w*�)���}����!�BTK�uF�D�b������(�����jM�{�;9O;�0pQ{���;��X7�u�Q(G�dnf�7)�@$
\�����%k�������oWO|r��3���A+���B^��u���FHI��A&����lt��A��2�{��'����(3�������?,I��$3�/�*���{e
BY c-2��P��`�jq��c���PSb�#
������<�\� @���\�Re��,
�]~�e��o�VL����70����B���� �N�Q�A!������~��`O���N�����lwc����N������[s��;u��R�����A�?1������A��$}d�t))\���e���cw��'��0��'M��s��&-�{pW-0�7�xw��A�)�f�rpnt������
���q��MAH� �u�Hoz��6���0�-�{x�}�A����wY��IU0#�0~���4~!����F����O��z'�arbaW��^l��3#F�B����?����0d:Z��I�@���.I�7�E���J���`c��Vb;��Q�	�d�A��>�����\��4�8�B�����v����sP���`F������ ��rs�h'N+a$�o�^L����G�Yc��b6����*��-�!�����}���-�����~���?������K
����������K��_�_?�����)����������i/���?����_?�0Qf��??����8�o��o�@%]~���@���x���u&[����!
�0�=8���&���'G��"/�
^��9d�#���:w��;uJ�������Icdr���j3A��	j}*��Pxr���>v+!��x�`-d��U��x�`,�+X��W��I���k��i|iY��iW�'l���	����I�'d}�-L���+Z���H��c���$�>��32��gd}rD*i�WC�#N�^�2�?a��s�*
������XD�s���k���M�����V;1���{uZ�4����^����m��#�^������kPe/�����;/����Lm���#���o�W^�~oN����������n~����or"��}��c��[�aVQ������Lo.�����|��~}��5�Ic�GGz��B��nt����i��G7�I7��=:c�Z�cn��XoD������(Bt3�>:cC3�<8��2LK�?:N�|���S�����|IV2S���k��3���
�N�[��NM�������qs{t���?a��368��4��36I������QU�Z�(�1�G�~s��3�<9#U*G]m��Xm��gdt��|"�6���6�(�1�G�-;�:=)�����"����A��c��36j��Y���a�<������?;������o��F�gl}t�*/��VBt�f�l��G�LT �����?L^]1�����i�vIk��QX�rA���?~��/�%_��������\4��K�������i������������}Y�g�X����A~F+���$l��������*���E�\1��;�Q���UQM`#��]�|������>-�����s�9�J���?[*��h/��[^��3����O%4�y�{Y����2Q�����_�7���d���s�n��������+D?�����*���e, �,(�r3'��2�S[i���q0��!���}���J�P0Z
�m��K��/ct���c��C/���$g�/��D�emAx>��Y��� ��?%v�@~
��d���_��(��A������?~��[���7f6L_�X>
e�$�m�y/��i���,[��
��t&�G����	�O��
�wW��Ao�ok@	�S>�^�@����mR����6�I����\��~��^�6{b���`��������$������v�4?��8����N��k���^e�������?��o�����*|�F���+������"�B��)X2�0���A��d�����v9�1�� ���?�w�CZ��Li�01��kB��qH='�#
����z����"����~a|F��2����3-^
}o����9dU��VY�Tk}���������(�����k�E��_���U)fxn}�����{M�{���I}�&}���|[��������HFF�7��>����fh�nJn���y�x���!��~�&+����{����P���>����L��J.������`�G�RU���Q�:
_
��B����}`�������X�Y�Z�en�P��?o�"�B��O���TT���W	������]����m��Q�`�j�+#�X�lB)E��\O}�*[�J��Qh�#�����tD�����^�2!�!���dO� ���j�4iW�Bo�i�Om�i��j'|:,*������(���=�$��ZT���lg��j�4�����.cA���"mG�	/�&�I��htm�6��HX��!yb�Za��ht���&T&~��V�l>Db�e��+���e7�`R_�#�SDuk���%a�x�89����"�'W���J�1H%�;�d`�R;��}������C��GW���~�Q��:��rN-�U���\�.+y��6]�����>�@�Dr�����y�i�r�8��1��r�=�"�{VzV�1����l�vV}���jX��8%=��Y�)���e��	rv�����fkO��w�~sp`���������t]�^4/�Fj���{��[��g�~C~��6���1Q~�
%�0�����TX��
8sg\�-M��zL�0��eN�A�m{���-\�4'R���,E�%��f��=c2����������<��D�S����j��}�{��hj��:��	O"s����7��u`�YlL���>�*:������zKB&�z�������x�}�����x��L&����U�qS�����Y�\#������j}��Z�-�����������O#�IM�`�����)���n�����-+a��V��Q:��,�s����&u����9k"����������$�D��S�Q�`D]&�� h�Vfks}��W=F���lj����L����f%�'��N��:�|�m�t
��M����'�D.�[���T�����W,�;�H6yF��N��y23���Et��MciLNl|Fvx%���A��l����D��H��vIu�xSC�j�g�zm��iK�q���i��)E�n�`~���i	�xs�	Oaw��OC������[|6���L������~�1G��rB.d��j9G�ZB�
����v5q%�����N�f��L��A�~o���1��Y���[���8��c�!��A�O������$t�h�7b��9���9�h�V��(��*:J�;��u�oU���).����Xv������x��c��Z7V���R��D�s��/�g��N��)���o"�eW��?�������?���o_������r�����r�}��)�O�7�����0�e���%p�e���Zb	����I[a��f��,��M�,�B�)Q�>�<[%Ze��f/�d�)�>�U5�
���X�+��1D|!�*�JF6����q��Wm�c?�&<�9
:<�	a��D����l��a5y8\����K�L���L��3���Mf%%}���/:5F�i���R@t�a��vj*S�xh��!lg���5o>U�������%��g����-1����L}�Z���-*820d�
�J�I�����(�"���9���a�e�d@]+6�Bc��1��YIf�T��F�����U`���V��e��k�d0��$:
.�%�6k$,O1.Q��ZY����v����SsZ-��jQ!�������j:[M%����r�NG8cK�����uQ��'��(vK�p�S�T�wZ
K���= �$"�������<@N�m&PN�1e����iTl����7=�YjS1�l����F������C�}.9	��W�E�s��w2�i��*���0 ��E�:�EFJ��c#�T����������|��mD2/�K�����##�W?��O��s���y���K�\�g�����f:������~�cm�?��e?}����dGc	L��H	�}�8�7��{�,.��\��O���.c���~?���]VGQ���u{�Y;>���%��!��s�	�8�����]���r�c1u!cc�VoI�1�l������/�I�0F@�]�7i����-k�Y� ���c�ue��
�h.�.vtl���g���(��,�G�<
��*r�x#��m���T�y�=�������2�0|s5�;��_$o�!f�	Xb?����v�Z��F\�c4?���m&u�~����q� ��J�C����E���!��p�wepJ�l����iR�i8V����C�)W���\�vro<
�_F�=�%82����:%]�~���"�3�X�0�B��}��t��F�c���\����+����y��7���_�~�����^ 9r\bB����b1��(���&\H1U=��@38��x ��.`b�����G#������5��=w7���������m�~j[�u�q��tj�6�f����������l��-I��R������������\No��/7f����8���Fbvg���E�@�����8����
f�
�����`��Z�X"5���J�V}W��%ja����-�������zQ%���P)j�%�|*�*��T�lW{9'?5q����������*��i�0����N�ZS�0�G��*�/���kJR"���@�;J�������T'�s�h�6@fb��k��Yv:�[��5O���1�*��iUG�E�����Rl{����\��y]�+�<��i{������Y��|ARu3�
�2F-����(�Ou�u��l@kg�8�fT����pU�������N�h��Ux�*1��!������
j�P!.6��	���u�V�������U������9*�9����M�OG�m�l���C?�x�r ���L���6��H��h2L��O����� ��C;l�n�;X�s�>�l�:{n?V��1!Vn	=�Z
CU=jj�ULq$U��s��W=2��l��%��:#iE�a%��#O`���s�b������7����b���*G��[�:!0��O����
mqX?Tf�.��|c���>��3�h)�7�F�Eb����N	:B�_����j!h_�vMX1p�����j�C�D�����Bi<�5y�*i����N���=V���q��w|���F;��w��9��tKO��u+6m���-���u#%u9��1��*�@jz�Q3�o}W����ivY~���[��B��I��rqJ����Y�V��UuG�J�I�K�4�m54%3����75��Gp���d�����G�
2H���EE��ho����b
��������� Y�4 �>���z�ZiW��s�������X��f�F�{�7���|����N����V������:������T�������f�(��?�F0{�~��7�����2t�d`������������$*
g"��}'H���<%2�BU%y��bIOA��0I��?���!2%"5��pK�hb�v��������/qH3 /qH��}[�yN�
�#	��
�(d��
Yy0.Y1�{�B^�T/��Y���T�j�'$c�r� ���#	6��q�#=�au5����5O�[Vnn�8d+���w,q5��T�%y-_��\!�&��P��E��"�K�B�,%�_%���V������M�~;�����D�Ui�~������"S����hz�"��t��lED����:y��Vm�S2t#Q���"w�����Pt�F 2i[8�V����Q
�
Y*KYT����T�i<s���IT���v���[�����xN�6�#�pw��D��V��,���"��~���]���r?�kH1��]�c[�����������+��G4���k=�V�S���@42�*�z%\��|���e2f��E�y�`{����L�<D�F��p��Z��&w�Z�tsM���2��FF��1���k�)I�J^K\�d���F�M�@�����kE�6%`f/���X1p�J&�I�56��'
�915����C�6�m�m|�=�"��TT�f,?vMM<N�hb�n�&Tov����15�����J������A�����W��S���d�*p�O����x�EfM�V��p%�j�H'������i^��� mr����hpw�C��j	D�����������]l1��#������CLFc/x�g�Ay�^M���e7��Y�)�a

4e���&1�	I��.[�E���3����d4���P�E��d��KL��VRi��$]��MKP�Z��=U#�������8o�$S�q�1`��S��$c�'ad���<A�$c��@Fr���b���'�
.	�w��>�k��H.���k��2��wtuz��M�8!�E��Of
����K�5��V[0o"JSwb�y���vw'6'��J������K��N#
,�?�KR����u�C�@�]�~�q�>�R���&m;Q��d��s���eNF%�����=�Kuk>c�-t-Q�c���B�*��<�.+�c���4.Y�t�WSL�jOj���#��$��~D��\4	vv�Y����V�B�����x��Lm\���AE&r[�1-
��ErIV������O��O���H���W�l�vB��W�(I���z�^�4����Z"4����S���Z^��?��$�A�����HTs��K�����nu��t+Q(ep���&��b��������+����i����0Y/�bn����v����}���q^�d]�~�P2HB�$=<�(_-i��������K�(e��0�1��Wx�
�dGBx��m�4}+��}���{�v����J��)w�4-��4�~������:C���������rg�km���X-q�u��&h7�	���s����p������Kqd�0��N��o�Rv�����Ht{Z���k��R�����h���
������l�DY�;���R.�x���l�,�?�������}�i��<JW&������u}����90������`u�����/���H%���w6��"i�����8�����u[�7#	�)����
z�i�CM��������&���������!Nh���e^^	m���<��5�yn����������bJ��~��'u����v�����n��:H���t��������!���j&t���2Q�^��z���7������TPK(���������}��p�������*�Z#����w�P	������l����0wI*#Pp���q�R���;Y��|�<<l�Jf|k��{�����uE���7�m8�	�wJ��j�$�o�q��l�mCm����\��8���(��-���]���	�n����$�
#G*�n����F�R�����`�e<��F����y����q4�!�|���c�O�Gw��P��
��'��P����#��!i2� ��J�����������s{� �_������H�o����#-��i�_j�Q���+K��6��7R�/��a�y��������������]=�wI�4>`�Uv�h�=��}�t���j��6���!��a���;���4� �~�6\M�hp���������._�DsZ�n��Vxq�2p6�j�Q���;[Y����(����:��r����4�����KE�E�/���"����:�Z��k=���53�YF]-A��e���C���,C��8Wi�+�J�O�&����������kt�D>��
2����@���~3��*�
�
�%b��c��:�E�	K$��]��?�0�W[]R~�]���%b��`�8�O��K�me��-���_��IT�R(�F�F�;9b�"�[t��������
��r|V(��Br>��5��9XBp/Q�v������M���FA�g�:<It��f��D$n�Fu��}�����j�?��9y��N��:�{�4S
7v�����IQ�d�������2�`n��<�W��F_��`�/��Yt�c���z�{�>3�+��z!��|�������SW"F�-<[���v��������H����k��.pp����������O/��~>��sb_��y�[�B����i ��ck��7������?"-�|����o��#<n��TUhu�}0��>u�k���>�s�w<���:�g|��(/� �7<#l�����_K������I�3�K�|\7'��oOB|����X�1�!{vM2���GQ2������7�������)G��n^��Rz��6��.LE���s�W#����� _T��p�Pv�i<B�.�q;e�����oS��G�=q���Tp5Y���8���v�R=�# �?����?�}��%��6��L������Z�Qx;>*J�����E��?*��r�tK���A�q�W�l%�:�j��,���z@�'�)��#_]�����o@
endstream
endobj
48 0 obj
   12683
endobj
46 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
50 0 obj
<< /Type /ObjStm
   /Length 51 0 R
   /N 1
   /First 5
   /Filter /FlateDecode
>>
stream
x�3�T0�����%�
endstream
endobj
51 0 obj
   17
endobj
54 0 obj
<< /Length 55 0 R
   /Filter /FlateDecode
>>
stream
x��}M�57������)8��U�[�mC3P�0=��,L-�]vwSvu�
z���R�P�"���5x����#e<R(��4���O������)�l�����]��/?>~��~�������T�Ln �/�AA�!�G�*i�}||����?|�O�0��?||��������_������~q����O��������(m���������?�y��o����z���=mm��_?���f�Ze�6��G�)�q���q��
6=�W�>��d��������l���g��P��(��w�i_P)��>h�MP)xn����k���Zj��5��Qp�+���o��(8A�V&B�a�����dl����g�D��
9�h
�%��0^��Q{��8�L^9 ����"��Ax�q1�8y�tV7�j����*�P����F<}���*���{��J��Y�����f^&����fe}|�T'��������f|�"����	��L�Em�����d��M���Ea�!�s���1EJ#xe�]��-����)��l
h[A_B��R���4����e�_�s�����:�w������W�@A�h�<���TP�)���H'������C�9g��Jx�j6L���Z�^�u���e��.�u�a��Bi�y�������O��e�������;�p'���2�z,��3c�,J}���yN'����-;o/�����Ua S|��q�����LI=������r���:;N'�N[�%� ����YzC�?F;�ak��@����I�x��v���vxg��`:5S�y�t������4����M��ry��q�d|,�M���A
����p�5|������@��Q8�*���pcl��	Rg����m���8~��3S��������y�$��v�p]��2:����<p�-<���0F�GF�1������
�~c/�/���kU�+���s��q� @�S�K��;�|����+��v����U�����"��[y� y���������l1���I������kLK�t�����%���7��u(�t]�c_��9�"���q�	u�i��=���D���@���@m���~$H
i9/&�B�QB.�
.�_�
�6��
Nx��� E�K?R���H�Tt�n�1�"M9��a��
�����?r�������������Q��c��m�NT#[\�B6�UF���n�"����x�c$U�������b����X�-��M�ca�@6_�I��sB.X�����E_G���dY�����nu������������R6+����:>������t�RG�2�Txc��B��Q��������R%��,Dy���-����} q�s�N������kP�d ���e1�oY��C3���3�c>�&v\T����Y�8�u���l�|k�|�$d7*c�u�.�`Wb��r�]xsE�__u�+t|5�;��eL��[[���5�������i�������-�5��:�,����cQQv�����yU6���_����}�];��h�5d�S? ���@:��!����U�K�������{����1[��9�����N�l����l|�`��o��$�)Z#��*2W�pX��F��tc��<��je��MC
��b�FH�t�n���_�����jt�� ��Z~ ��`�L2�Z�UN|*�J�Nr�5�
�+N����X�D�8F���:�����-��;|�����V'�\��b����w������15�����x��K����VdK��D:����7-u`5�?&~%����X��<���%�E����8V>&g]V!�K��Ll��t�X�M�E��6��y��r��J�
�(H.��6Iy��i�W��]�@d��!k����4\���W������q=J��m(?������"
WXc>�2���(�"�5&/tI}�|q@� �V����CQ80��#���w���F.���|���|�P��4�4�6\L
tr���<,��U�q��n~���KI��z��L�e�b�:_w����Z�����h�a=%ZH�����8e��pR7��*��rJav�8�u��-�� �N�-B
���;r)4�2����!R�"5�	�N�7���@�9� u{#|*�oTw�*f�Zn� �t�X������^KN�����^C~��<~e��U)�!�	�7���� ������Bh���4'��k���\������lM
��������tm�\}�l��f����5����� /���,���lS���}��{� w����/�P�$���G5~_���]����7R|��?h%�]��M�vZ���V�t��'$F<	+��L��:>��#by�r��(��p��38�7s�v��t:���N'#Jj*�(��c��5]��.�N���9�����	�YuR_t��z��M(s����J9��������S�Y���V���S��i"��_j�����eO[��F��~R6�py����r;|�!`���-A���1���`��w����1k�x�^��M7b����B��������@9�������#�b�D��<�t�kqs�K�;���*~7�7���3
<G�9�*(Lyx���>xC���N��j�!g����N9Q%�n���o�%��G���%�����;�n]�� G����2��J���c�"l������1�.�7�B�S�N�l_C�Qy�8��8����`A��v�~0b����'�S[B_�8s����8�(o��y`�Q<����.���	��u�<DA�IC�1:���&-���#��[T� o�pBT��A�A��^�FW�3������N�"u��?$������;u�v����
�?#��M7������������_�G)|���SAAXi�A	�b��m�"�Z�\4,�j6�]	/����0,�JZ%�]r��������S$tJ��ud*2�DJ�cy��/��;���H+��V��}
v�
�xS�^E���Yr-M�HZ���-u4����e�\����),1(eE/���,l����g��#��#���y:Ao���Q�NI�c���8��w������|�_����^|�A�v���YM/����2���f
�`��#@���������a��������p�R�N��t;,�
�2����aoT%��;{~^�������\�
[_$�d������Lx���<x^��@�b����aR�yvR(|+�������d�F�6�
���%�����c1\�8����K�YN��������:�����������"uS=�0���gj�nOE����F�*�t���������+��>�8`!��$�X�u�q-�����!�����r���� q��������'�o:D���W
%����po��������lL|_����x���o�2�[t��-�T�S��X��*A������Me#
FHN�f�5S�y����?*�V�j�����Fh*:<pP"�4rk�6nI��y��u5���;e�y[�����D	M�{s6@�������F�(p���N����J����x�K�����&���x)��������	�[jl_��w*$� �p�a�;�F���4���1���b����TQ�nU������ (�X ~�i�* u"��zJ�u\�{A�Z�;���o(�C
�'Q��u��S%�q�4����{�R�g_#�'���E���/���w��e"�AP&��Y�5�����MG\�������*R�}��
i��������SZ/L�x�7C,��@�����(����CD���L�@L2)�v����d��C�1U�^�@�b��!>�R��=	�P�����?���omS~�M	b��H��s������dU��>��m��g��6Ej^r��E/���P�n��t�^��SR^9���@!l-�j�mz��m7F�3�c>�0�#����2�9��}N���\	Me�|�r�r�$x����ca}Q��%��AGR6�p�� ����t�������o!����x�A�;e���;G�*^�X(r$�O���a[��P�H���������5�HJ�.e�x^0l�!u�J�kq�L��p��IS��f��5hS�.Y�]�.]~�.��KWhqHW�N��g��t�n������J�|r�,��Y���	��|���)P����bgK�4�	��o_K������H[>��g�~qg���h����f�
�B�,'���m��-6����W��y�c���7����F������Z"G"���%�yy�M�"��H��aoM��^�'[��QN���w�6@�@�&r�	���H�w�+��[T;�R�7,�	�[�|_�S�������v��_�������o���J?��0��/��>�g>�����#Ds~|F�R��O�� �����??���_�����1��q�
05l8~�?��
4:�w��`�YEK��O�������dMP>�M����j[o8H|���X�&���@��c��(Ci������|����r�@���c��#P|2C�,_��T��iBO��"�������+�����hN����q*�F�*%]��*�f��t|"M��L^�4!�'�\w1=>�+@���m�F;M�����O'l{�����g3a�G{�u��GS��H�D��K*�0a�G�~�U~����2)�gd}�e�~4��=�{
I�p��G�~�Q��C>���Q��E{")�2�������P��h�k�����?�c�(���=�%Q���	����F�0���h�o*��}�G�~�.&�4��h���*��=���~#,�	{<�cm�v(��n`����m��s�:���(�x���e]�����*��)�xt����mO$#������m������"�a��G�Y��*�b�=�������#�._/�������=mWQ��1�	���&���������a����
lTA��m������&��h�MA�0���h������x�������R�����C5q��d�4^3C�G{�zZE�����q�����riZE���������6�a��G[�Q�]�����I=��x����x�����XcTLS���{x�{������������h�����W�jC�:�G���)=�����>��~���>���/_��#e�w�������<�=�����I?�n���A��*�/�����O��WO���'$e���?>M��5��~2|�����o�P�=W�
��y�	*��O�Uv�>�y:}i����16�-�� )p_>����������R4&JB�\�x�������Ng��������w_>����3�~�G��%l��W^������it��88g��|�M�����qF�TDg��C�����!��"27N�&[>Y����<l���/���c��7b�sO�&R��F��r���*����w�R��i���6ln<��M�n�2��W�!~�o�R���fR:;~3+�'w���cBFgB�L$����h�Y�HVF)h?�Q~�-��h���F,�|�q����]�c��������.����m�@5#t���m���&�E��)s����r&�l�r�����l�Be�b<^�(�JC.�~M���^�����&��q�(������
��|���J)�3�LC���>�
n4����}?fPs�e�T��$�1������P��,��%f��g"7����zB
�/���K�9�����
z)��SE���q��<c!�3J��Fe���-K�����_?���In�E!\Dm�����|w�����*���m��2����_>Dx�XT_�gI��%�V����U��3ZC���UJ(��;�����@��C�A�n�e���]��}0]���kS�D��C5k��,�o��q����;�:i{��`P�^���f�� ����3�:�0T��\r%�y]~��t��Jx�4��������EQ+j�
�J��*w�}ZSC�v	6G/��h�'��4�1���s8I#NR����$�6�n�YS�@���J�q�jU�Cr�OS/�����/�;HS<�`L����Lv)��e��8����)�f��y�<��I��1vL���*vim�[mt7��*��aff�sI�t�
�*�"�q$E
^[��7����� ����"����@���|��3M�?^1�cZV�r�����tE�d����1���s���a_afMUn[�J�-������i!K�$��P6J��\�C���=�\�����z�8]��+�������o����s��f�@;���h_f�LGD3���z�����@mr4G7�j����.F~7W��g�C�+8��U�(u�Es�b7U���"v8���U'wxn;B^����������g�.��v��=sb���mP�MW�bQ��)�Fb��x������	8��*(��g�_@�)<p��vE��	�\"g��44D�a#_)�����o����)E����ZM�8��)�X�7	�|Jk�!%S�{7�|��~����Z�xJNk�1����;�i��mq6�����?f����U���uc��Rq�R������O�))�4L�]�{J5l�2���E���-��a����6	J�f�~��vW�>c<���6t ~,�
^��1����a����������"�m���4D��2�}��e"w5K������(�S�*pWc��i��W0h$����#�Ra�����7��gr��5Gs�1
���k����[;Qo0)���ah���Z���1q��z@�xM�q�k0�������6v�8c���v���49�A��>��;��R���.��)���}�1#^sZw~D�������al�hw�����/�� ��-j��t����?����f���:N�z��/��|�=�A;x��d���JR��G���Z�@'�-`���\R��s�v�U�G�-�,��0;4u.�p��D����D$�������x��
�D~��)��U�Z��?����}�5�'�]���l�#��X���bHc�c�@.�G��}*�X����@��l�}����
)�����[���`4DW�h( ������r�o��Y�Zct��;O[��m����G��>_�c�Z��'v=z�X�i'�����`rD��|�
�9�v-�ZE�����S�(�o��z���'��`ZYX��"�w�����/v�/�#C ��`h��t}���dL�{��a%&�D���k������������6/��v�y���&<<0��Mb�g~�-8g<�tM�EK�yW��9T�'���Z���c�|L,?�^���������u-D_V����y����n����c���z�~s#��;n�����j
d�m#.�������/��8I#c&��E��0%t�@e�S�]*6��N��h��*���+U}���0�m+h��&+�L���w�4gX��������sl�����Y�>�/W�$4��g�"��G�"��J��y�nxan`�X����1����6���J���Eu���ZDWWJ�I�����$>z�K/��h���~�6n��ud���|�2X�c"3�Ke�|��Yo�����{����L���wu�7�	=bG!�r�����b�fL�3�sy��>���k��n�K��`X����l`He�B�`�n�*�����T�u]�����
�w��89����.�����w��p��UW}���& �L�-����V+��i�T�X�f5&|YWK&Rs|�e�J���5�r��D�5�����>e]we�.�����>F���'"\����m����G��?XL�R�[>c�����=S��wXwf-�b��_o�)�,�b\���s���UP#�d���fG�;k��e��������j��{�8M�ec�N%�����J2���-�8�����W[%)����%����-���q�O�����'���u���1�H:���SK��%cdo�[���C:����h��hU�����\���B6�'��S��*,���13�7gY���nuQs@��i����z�^m\����hJ&x%�k���i(��.q��������QlY�m����-a^�H=�]�5!���6[C�41{���5����o&������M�mI����2K�T��U�F{d�]��-�L��:����r)w���h��F%��=���HA��F�w�Q�i7nn���:�FA,"�����\rP����O���o�}���O�i?��KsTt���L%���������]I4���H��W��_�V����!��4L/����y��$�K����No��U��n����S��Mk�V5�h��������KQk[�d���5��ZZ��J����Q(�����ird��Ha4����z��J|�d�Y�N�1��nV<���i�.�(gl"*(�L�c�n#:�L��=*������BzT��7�8���������Fp�������^9#De�Go���o�t�X:	cuW�)��O��[�N2��Ws�h������g��K���RJ��b���L|k��i���Z��io����T��z���Up�k'��v�f��py����k?�b��������q�a����{�rW��~�<N���[���o�����3��\�a*���'[�n�������H���P�K1G��Qlv'*��!L{�f�;�N�^��i���G i��cpO[��;U!����K=���E�}Zb2�F{�0<�#��|*������b�����3R�
d�l�4���lK�c���i�����BZ�`�
��o�L���V�����N�&��Y��NE��uz:r����_�.@�� gh�#d;�e	}�3q�B��$6x�����A�((��\�����
"����(�}�v�9�i*f_�A��aJl�*^��pP���x
�7'~7���Kl���<����4;�kL�<����\
���/La.��u7��9j�OY����l���i�k����OM8�/����$�T�V�bN��G5�5sF�F�����4�>+fQ����-����x��D�&2�5#q������;��/�����<*D/��8�0��
��rg�Mr�{�o�"�����`��Mr�A�.��b���Mb���H��T����<<�Dd
�eG����#n�.���)����"�����\2������������.�p�:�u����6������s d}�5�v��px�Z��.pu�������Y~q:��t��W7t������pE\�l�����:)�W��5�21tb���j�����HC`\�7�W��oso	����k^Tv%��O���,8�j��S�s�M�s���v�!��Wa*/��?3x��2G��>�+�������`�G��1�Jp=\�l,qqN�����yE�8�;��_}:�5���q����&�o����K��j�F�&%�&(1kZ$0JZ%�o(�����U"h����#��8��+��IE3L�`[:u�%�q��I/�s���6#)82:��z{��u�^�L3�S���z��6���N�>���Y�w%�H�+a�����P�E�7��SQ�g��q�����;��;x�"��~+���vOj�t��!/��s/pisO�q/��[ ��2Y�2���3���j��"&��,0NmZ�~�H��qP�Z�Nm�v��HU�N���[ ����alY���[���~rQ�w���;7�u���XS�X�f��HO+��#f������G�O7�Z T����Z[�Qpi�	��>F�K7��}mW�|(��f��D�����������c������������.x!rO)�� ����c��%���.!�������F���5����C:�>{pt��Y��&������j�7n��6S�:�WK���$/&�:>���b!����}gVJ�o��[;���#[Y�	&�
��F���������t����^�q�8�p�s������F����~��B�x��b�&�Zp�*tNE������$����3�l����s���K4}���{���ly'��6-{�����T���]�Z���2)���e��^����e�J[�'L=�)���D��xG��eiM���H>����U���q`Z[��0q��c���a�	�����D�U
����8TM,�S������N>Fs���nD�{��}Z.���_zPv������&��R���ff���ZC��&C��!��g�$/�45w�Y����e��,�m�1������3���b�#��_y	Sv��������@��~*���-l1S�	�F�1g6U��5�#w8]�c���r�%� NH�!��	�a��b~�z������cu
l6G��aqM�;E9x����sq��]���?��.7tw�{����~
��g��� ��G7��B�5awk�~��M�p��'��Z�l�$�"�����\O@\,���-�o6�^	�#Vo�{dD�p��o}�a�F�5�tH;5r?	YE�������4�x��]�������>��'WZ��=�">N��^fE�R�mG�=K����YRb�������* �n���1�u"�q�h����S"���A������H�������;;-��4$�Qu��l��b#��I�3��L�&�q�k��"O�K$y%
I�����d�Zz),e�J�q��Y��[\Q���D���-
0�%����{����P�q��4�GL�8�F�z3����qyy�!���@�NG��t��J�]D����&��K�#E�����Z������;���#W��C��L���]$/Ur��zx������2�m�,�q�K�y���J"�������R=K����F�����H�k��rx�=Q�1g�n��H��Y���DPf}�����2+�-��g#�J��o��'#�c�����'���#i�Gv,5_�J2����7���V��v�|�)��uJ!��Gj�I�=���E2���_$�&#F\3�[�������u2H����x��>'#K���=���@�8������]����������|d6m6R���a$������8�'�����Y��'R�c��H����U�����=����;	�"[6bP���\B2��._�]_�A%����0���w7�!�N��_0�=��^�o_8�aX��a�izRKI���B��~����_����g�f/��
��R#����!�!��!^�]�~X1����k��#L�K�q+����0���0�����S(l��go���+��Yy����B$E�
n�*�q����\���2��}�T2���23�i~0���x
�q�3��������j�4��OP`h!�L�H�xW^�	?��F$��B8	����/=����W�6�BvK9�rMH�V���u�q�j�U�FV����12����y'����_�O�O>�^�2���;�f+�u��t�#B�R����Q
k��mwZ��,9�����!��R���B���vK��/9>x������og��\9��)�����t~���y���Bo<���iL��r������c��e�4� _$73�\�'�6�e�}\�����^�DR}�X9g��L��3gS����~��������o��=
��w�>6�M6��
�^[�b�|�n����s�����e@��
endstream
endobj
55 0 obj
   11653
endobj
53 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
57 0 obj
<< /Type /ObjStm
   /Length 58 0 R
   /N 1
   /First 5
   /Filter /FlateDecode
>>
stream
x�35S0������
endstream
endobj
58 0 obj
   17
endobj
61 0 obj
<< /Length 62 0 R
   /Filter /FlateDecode
>>
stream
x��}I�m������=I������iB�`(���P��w��M�N��_��Ph-Eh�����n���H+"�N��C?���<�#��l
��������]���~������������R)�����uV����#j���>>���������a�������������>>�?���?��O?��������(m�����\�������?~������#�o�������U�.����xc<��!�`�!&e�{D�b�����rL��������a���a�\1�{����.Yo�i�	*/M���^+����+��W��v�\�*fqe�����W���RrI9}Z����WE�&.��C�p����;��� MAg_W�]�T1;�P��H��l��1~P�K�v!&��b����D&�3��M�3E�������"y�rz]��X�V	��e��u�8�"�5���	���u�6y�Z�u��}���[5E"��7���O@Sx]g\��r���[I��-<���2WPl������,7��y���q����<��Gn$�EF>�l���#d�0���c#	��$������,{e4�C�o6X&���
�	�����
,�����z���*�~��WJ���KI�\V%f���n��C�������7��w�X1e���|����6�>���h��8�
����W2���'�;G����B.:�*��	a%T���_�v���&�����38�UL�v�J�r�C3�"�x�eI�{�q��}mZd���a��#� �3�������9�C���/���0��UN:�0�.��Y�����4��s�q�s<��u|<���P�����4z��h�aVc�n�WV����d7��=��1f�<��������T���T�8��N��BnIS
#kN��r2��7��6��ca�F�G6&+����'�x�(�|c���7	/X&��\�%���y���g�*1p|G*���p��W�C�E�����s�q�o�� ;��uo!��H'�dFR��*0��F�0��)����� ���D�[�������<�=T4�����;A�	��]�7r��H;�h�J���+r�k�#��`���D=��r2.���a(��<����I�*�M�?������	��s�p�?���L���A��/�7o�+�)���`���6v(2���v�������o��� ���U�!Od�
��O��~��d��5<������<Yt�	�!���b�:d���O
N��=K��
V]8&�]t�D��#�J��uE��PS���0J������u3I�������MqK�P\>�#���+�����a!s����U������ca����n5v(������J^9(��������%c��O�i��6`4*[�6a�kp���e����P1����5E�hR4����At��A2��"�����,��'�8�)!o����/�G"�n7M�H@����TxU�bJ�mB"�Q���@������ �?�a���/���T�u�T��%�u��n�,*P��]�9;n��?���x&�����a���{ib��R��d��KH�`6��w�`���N^Y��#�s\��!��y�H��-�.9'�2��.���"Pb �1�zp���<��bL�k���E�R�i���{�*_6�"��!v(�����&�_�����C�b�"�j5S�������*B
��m�>�Y��0�������^Q��+
1Ha����z�3)��?~*�����	d�I�!�%�VC>x��u.t�5��0S�[�5l����K��o�W�B�����	S���L]��a8��a�D�}�k��h��>u&��#sY<Sa�\&ko���u�69K~Q.�k��{�9�\�Q�����B��M �q����W�I�!k��QT�����}�-r���EE(�:`�)����P\1�w��qr���]xhuk��~D�������b,k�2��mi�]�o1[�]/�jI���g�A��=�E)��E����	8KR�R�=�'�J���Md�W%�N� ��/�Qqb,J�?��+��#3ZW�����M{O��+t�-
��]&��'��_e�4q-����m����Q��s����8��j��|s��W��5����P`���Tq�,�2���PJ�jTq���e��l�����8e�����e�W���t� C�������$����<d�VPr����(�!�<�*��&Y�b�����&�K�v���?nM�{)R�����{���a�"0�0�27����h�a>��Or)O"X��)��kKpRK�9�J�Q<9�l����pJ?�r��A�&�K8�$V���W��T�����L��)B����y����������xrc����2�������1��`�/�#<����T���hU� �3�f��ja^���=|m�|�e[��T?��"���d>IcKR��I���#;E _� 3=q�X�����d+����l��rW������?�l����D"�J(���Q�����s���]	x���9�[�' �������y'c�;�����<J*�}��T^6y��;P�x�
��0V��~������C}�xa��[�k���4�7/9���&Y�o��(�S�M&e�;�2���1��Y'�#��&���|�D���.�I��V�;k��~���o��CpE^��<������Y�
<���I�8�sQ%���8��x��������r���^��~{Fd"�#��k�D81�=+��7��7*D�/o�"kY���I^7�<&s������2�r,^�[i�W�V�6A���Tn����Ge`L����Ce���"�������rx���d+�Z�ma?����*�o/q�[��[����0T������9���0��[�o��_�#}�-�p?�]_H��lj�k� ��v:������?���j��yqb��+h�	�D��z�_R8��EA��mpU1N�u���l��b���7����"?�S!�6�� ��~.����3����],�V���D"X�~%b�@��O!3�����&+��f?[�h����"��7t�CX�w����
��O��i{Hs	7j�c�+��`k������F�?/]'������8�W-%�],C��j��6�T�������%<��������$/?�����IW2t����kHW�T�L��on�de�w��C����AVa:1T��:��t(#���\=�d�p�$07�p�~�j��:���1�D8�b��e���e ��S!1JS0�,���&���B��hu�F�xpq�HzG���:O���C�����������g�d' �$K��>b�����A��I�rs-�A��L�<�]�K
��`p��jj�����������6������C����
B #EB
�
ddV&0����a��<^�R��~�H~���"Pb��3N��i+�I�����Z,UEf�C���Y���Y�T�hQa*�T����:�?���m��T���8�)�_�|,~��/N����[_|u��O�?mEy��LV%�z�y�O�	�����2���Z�oM��+W�%����Rq~>��,�����ze���Bf�G�e��0I����e�N��L��W1yC�2��D���`I���u�!b.���� Gx��b��K,��

Ce�)�� t�C�?�Li.�������L��[^�C���9��<|S���Onr�����R<p2�1YBF�T�����*�r�q@�S�x)z=����T�� �0����81r��O�H^$_l�KR�|� ��)��:n�d��������N��i;�M����M����Chg���j��#/���Z�v�Ix��`�8�pon0p�v�����
:*���)Y�����=f�k�\8s'���_p�(��9���;�Q!��[�Z���|?���x�e�7CK�����*r�T$?��HT�3�q���MM�p�
c���1�b^�tye1�a�W6��v��,y�X����Gx��t<��a&�Er���{a��#a�hZ��x!8��o}�pl�P�G�?��5���?��C?~�a���W������������<����U,���J!?�����+�|������w�������lj�s���`�w���e��#�-]6>9B�=B�������OdpZm�����'G�������U:MrKH���I��v(
F�L�������R�xr�:�9O�������D�OV(�����5��|r�+�^=%a����7����m��
>9�e�� �.��g��'G��?9
lq�hK���	
�)Q�xt�����?���fe�Y���y^�T�������Jq��Gg���/j�?9#C��M������T@������9*�C����;�*�������`�#��H��W�,�<)��������ZY��y<:�����F�=za����`��3�y������3�]Q���������e�C��y��(���`�����+k��������.�mO���N��mt^q�*��b�����S��o���MY����9�S.��I���:�X�����-N��lt~��TY�M{rD�nyy���(��!��b����B�������k6I��Vl{t�Z[m,�������T�=:��3���}�����������<�7*��f<:���
z}���,��Plt^s����}�������r/���<o�1[���y�Y��}���\��b�+�=:�9k�������\Z��?:��he�����y�����m�^�H���H�����'�b�zy����_�������������O�H�U����Gglp��u��������h����MI��h�����V�U����DBz��;���qL�.���Q��W�,G���������?Hj��4�1���8h���&������_��S��y|������3d��9�o������fW;�=�����g���o��'����o�.'��E��E�d]vlJ0*�gcc�md�����
��2���7�Pp6��1E���<�����Qp���k���`��O���wE��}A��u=m���w�it�?�?�_���0��A�
�46(�|�`���;������}fo��zv*&���s
��)��o����M_�*�)��>�FAj����r����)+m.|��D�.�d"N��-5�O.�l-�K�.��K��n��>>����T���v�����Zk��|���K�O��f�P��� ��}�d����r��\^�8~�K�c�����h�F�,���))��W`C�d�,�����H}�����s����v���/g���i�41%��<!/F�Q7�.���w������������ge�V���jB^�������~y��cya��M�./y�B^P��I�B�o�)��4F�����lRjRz����Y���7�4U<#�����J#��J[�,�R���s�<!����1����
�2��zg�_�����D9��J�����:�'w"r=h��v�r����X#�I3p����k�S]�-3b3#�����A�H�l�.eL��f�����b�tN�XW�{��#c�d��<�<&�2�0�����a��,{��<�V0c�x4L�/<�tvjS��R������V����L�[�I�C���t$+�����n�,�A H���p�?��m�+��<]�n�	w��j�����JF�O��55���e�����N���Ob�.����k�G�@��������m�9���E������\��8��Lk������a���u����qjr7���.Nu�4�;�	]$�s�Ih9�HpU��d!�U���s���\�K���k���4M���YXc�����pV�l��W�G�x{��Tt9NiN�p���*���3j�+�!����:��������������| =M_��o��3J�5�S=�����+�6	6�Mz1���[/�g�Vl�G���P�mJIe��YEt�.���Mfq1w��b(���^��	7a�����R��N*2��J�a�mw�
��[;r[����[Ae���Z'���Po���#wP9��r�W�}<#��?��~��N�9�tTq:[��4��^��~�g,+����������������(����/W�vX��}U4c4MU�O��Afq��w�@t?��-s����b����T�-UC�M'{�S������!L�vO��v������S�,�.�����FI��Sn1�j���l�)�����-H�[��5mi�U�~"n��P���������!����^q���M����HH��W��~���^�A�|�+6�A(�.fF����9�=+��!���b�Q��)��Q�t�/j�6��bN��r��7lL�Pg
�Jo�h�/��O��B��N�3B�V���b���5����b�`��M����]e"G�3Ly�E�x��W�P�5�F�6��������L�<�-��6�������-{m��4y�t{Mk��$$?���}Gy&V����BF��X���y5���9O;4��[v�5�
��b� _�
���:�7JuG�Ygbw��IY�e#��4{��^��6�Y�q)����G�����������^���K����������k�����tl��^�p��j?���y(�Y��}]@�eR���FRN����J�&�sS%o(���E��e�@����hF�)4�{I9]��m���*R74+�06}M:��������e��SR`�����N���%*A��K���;u���yjo#�`��9���� ����pc��6�&D�oO��Aj�gR��9?��~z��\O��u�Me0�I������y�NL?�)nY7�e�u|L?yB�ju�sC���'o�m4��a�m�1l�O��{�&m�FVD�>u�v*����N_�p��yx�=*Bd��vAj[_��A����Nt1���u~��g����c�gHB�,�&�R�u���S0���[iG3Y���,����d�x�a\M�T�0X������~��G����,��%�R�F6Tk<�i{���s6���Za��\��c�&��0wI������V��]:j����<�yY�PS+�'���� �S�0.l5	�t��,1	"������&��)�) �)?M��lE��$�L��KBJ�s���B������lE4�C����M�8�sv��hM1�b��p3p�c���=�e�L�0��k^�`�7pJ3M�acN{O��d��-�pU y*~zB*�~b5;��Am�&f����f
��w�����rR�L���-���u����=X�iT�%��>�{0r���9�!�Tw{8���%��4�5�Q�\4g�h��7A<U���1�=gO��Cv�N�����v��3����{EH
  �&]h=8{����.����Cy��[v>�9o[P��x��e0�:���j�)�qCR��h�N/Z���>qFp�<d�L��-��C�zM��2���?o'J�2!h;�[=s�u32�m��Q�dD`�	;��M���+�l�(Q�J�	9��P�T��S3���c��k&�!��v!��di��
��e��h����<Z2���|����^�yu<�.,�g],�>�
����1�t�T�17ru����,���\�V�H���
/��u^�\�#�
[������Y�k�/q����l�����b��������:zV_����@e�����b��Kt��
���W}��m1}D{OhmXa����#����Tc��S��u�
k�-��
�_�������zD7R.����u�9)ct���0�K���&�����s~-\�}nC-����9o��gih�����F�A"��p�2-1�����_|�^��-�M���oS����p�^Y�����elh����3L
��7V���
\��A{ ��4���-�1'�}RX�� [����L��ZG�������5U�vj���?*]����0���S����uK��p|nk�.[���(�$x�=��M�62�e��4l��b��V��4��`X#j%�Q��9�2-���
Z���3h�����c7��
$�w��-��
	�E�.vDL$����EO��VG,%1z�c��'<�'�K�G�_=g���j"y��I�%��5Tp{��43�I���~��H���jQS����U9���g����c�uom9i��T���[��?�����������?�4!�']w�#�*��x!x�b��5���7�%yk1��L1�����~�y�N%�����@;�[	�U��p�m�L�f�)l�;�k/{�i�bZ2���g����e�f�w��3�B�X�n��K����G:��X�d��U�p�2�����V#�sj�S�1���d �9�l0�i�!�������8���Iq������L&���5������[X.���%�;�	�Qz�������A�C�$H{�)Nk��9R��Tx��1%8�b��
�.�f���[�V�-2���rGZ@�=%�E8W0�[y&u�D7,���&��i���Y��=����3�%��cnRa"H��%gm��w#h���f��*_�	����?@�V�#b���&���z���uL��M��F�j>�1Cd�um���.�i.�Mw�vs89��^33�r)r�f��@�M��n
4��q������:5EW���EH�2���`&�v5����L
������r�`�Y�W=�\�Nd[���n���U%\c���I��!���J�42_��~�F��&B���#���X�����2��6��r�AW����3�p��W��/��B
(s�m�h���{��q��/���4�K|-�i�!Kl�5z��fW<cH�nFz�o�#cF�uts1{G7�Y�dT���`�B����unA�U��������=�����\�����v��0��-��9n-��n�������w��6�V��r��Mw��e����9���y-�h��n�� �#�7��N yA�I���b��KeC����k���
c�EAv���M��*�C�����t��2������"���	�����J��������P�j��1� ��A^����.�~Z����|H����G-C����y�~��0�r���i��O�0��B��)p_��[�@c������z�GK�nuC�Ga�0���q�=Mk ���+R�P��tqhM��U~���������0�e@x�$��Y��fG���-�H��pR��b�u_Xe��:) =V��=�9t����L��V=V��z��V�emm�eR>8��Q�m�)9�����j���g��7���L�X���f�|�b�!�[[�cx���V��v-}=<��jI�jt}�#z�An���_��#��'qn���'��j�����f����y��2���*�;������KHLR��
���[c+�S�������K?]j�vq�|�'�����8.&���;)������|��2�o'ej'�Ftf���"�k��Or�+�7��Ls���r�HO�������>�Z����Y���c�����=��a�~xP��>^���-5��~���|gM���M���A�D�>�kk��[������ �@�c���I�Da�~x�#�C�.7u��%������4K�fY\K��r����$���Um �M��;l�7�l�d������J�MP���P�������(����2 �V<s�//����{R����u�U8JP���T��	����.{Z��v���Vm����������!�%1m	��,�jZ&��
j��[dv�*r3I�l�����-imv���B��`F������{W���	]7J���'[�K��h�/���f7�[O�8�ygb���T��`h?�'M�i\���4�`SC{S�)����y}�=\)�5m@*l�C;���(�w�%Z5QS�! 2�{�e�����&/�k���Mk7�����G������/�$��V`�|������D��;dp�#�!�)�^��.�������Wp��^����)��z�~��&i�w���_�M��z ���4%���w�T����
bON��dWZ�@��gThe��<����,!z���+��u���ZL�5��9<_�*��`���Xy�fFp�jl���^��/��OB����Mv.Y}������$��B�Y�h,C��&�x�f�]��c�Jh�qm��t3�;W��v����j��N��g��sj�Xqq���Nm/�y�Mu*3ntS�#��}S�d\��^[�;,s��M���'������]?0��&�\$.�+���^#oL���S-��+��mX��������G>45!�!�id��Q�����0�����7 HoG���� &�>��Z�����w)�l��U�e�*��w���I�U�����M�.��RF�X�f���ot��""��su���W�8�u�t���J��u#�Py=���Y��j�
1���+�1���';b���
����v0�n�!�{}�1���|~�o�O������\b��)`���cH)mH�w&?���8��B�K��+D����)�!v��`lIMm)7�����'�O]?�J�!������?0	:�����A��;����%����W]\5p����#Ga����'a���	.�D��c\�%��7
�)7�'\<�D�����J]D���hS�
#�oC���	N�~���0���iZ��)�n~�9Z�������{k;�ZL���7���3��cq��l��.��i����]�4�K��"���q���8;f����0����6����K���i>r{����+��%�Q�$nm�c�KP���HDh",
OZFE��Z��6��s��2�����^���\���4���a������N�+�6Z����O'�I1��\��;��]���^r�����t����u��)���>�����YE�#���I��j�~�M���k&���xw8�v����H�������j�WV���~�H�t�S����O�s��G���,����~�"���94���/��}��l���&4v���UPg[7���1��|��bN��!&{I?��@���W�]�V���Z�w*���i&�s�
����<�V���w��S1�):�F���@�~7W�d,�x�|v�nU�{M��z����b�'C,c�����J��Wxh���);7��,���p�.�����k������l��K3
�__�%����7�k9����P�Qm���@�x��(��{�U����@,�'�����e`����������jv��]�S_H6��������NP�����`��
endstream
endobj
62 0 obj
   11294
endobj
60 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
64 0 obj
<< /Type /ObjStm
   /Length 65 0 R
   /N 1
   /First 5
   /Filter /FlateDecode
>>
stream
x�33V0������
endstream
endobj
65 0 obj
   17
endobj
68 0 obj
<< /Length 69 0 R
   /Filter /FlateDecode
>>
stream
x��}K�-�q�|��51�N�M�����@@�89�
.���+����|�d��
��-~����b�lx��|}�K�l�Byk#�~���?>d������o~��?���?~���� P�/#�1Zo^N
/�4��������S~��������/���_����_���?���o�������w��B*��e�����?�������Ox��M�Mn�O�?���I^�����e������z)#�z����?��H���N���� ��%��������p�����F
���Q1���]'�3B{���W&��w�����3:�3L^�T�7����-�q���(����0����#������?�#�,��8c�8;��WLG�D�\
p���s��������8v�y�?�xM���D�l4�
���=d���@�*�Fn8��u4����U�g���M�8R�m�tE��aq}��#7N
���%Ep������H7�p��Gg]����F@�jhi�p�<��(F��+��A�`|�s:���hb�k�1����{�,���ad����[�Dcl�V#�������`&���-�x����L�Q��
����W�7�(8t�`�],����6�.����p����sH�{Y`Q��RwT����@�m�������.+�����$�qs��7#�`D<{'%�$gj�V}W�l�8��`pW6���N@�+�F

��0L�nt����9�t��A	>�dc�V7�Yt�F����u�_���I�����k����UCw2��x��{��A:[D�\|'���r��]�cL��O�^��������h R��i����er��6��P�X�M��i�c����wQt��~�ZA��sG�'�#e���^�*��5�Od1=ml���H{K��M���V��W`��&��G'K_F�m^_
�����:J�/}���v�����h���q�be}�V�Dt�T���M��w��GZ�]u�����D2]�k#��&�<����C���������q8�0dpaD��T�;�:]4����4���z�A�~�M�������pa����Y�����N[$|5L���/�O���(HG����v��xau|�#+�d�0�����x��u�C@���1�7��R}u��:���f���E���C6��pC��+�f0{`c��wFF�I�����3:Q^(k����!S��aw�h�uI1CTJ[H]ukS{g�|�����L���	��Y1�!�����?��K*m$�d/k�1�y�N'�*2ng��UUa��FI���`�W�1r��F ����G���j	�q���*b����lH*�H���TS�5��2xf7GV��?�:�O�Syv��v�N��l�9![���M��������w2F������sy��g$� �N$�����]z�m"t"��6��Ee��B����zD���H����n���a����J����G��-/�_ ��3��1~��G�)����S�c="N�����8G�i�!���!�dke�-��i�Fi�f'?F2��u���v��2��N�'���ys�0`�� ���=�is."j$2�+�dS)c1[� )0������Y�����o��#�������"�Nhr��+�sJ���r/�2]���C�����Gv��hN���i��z2����l�9![L����w���vuN��'k�.�m�V� �Y��H���+�lz��g�=�qpHp������5��T��OYJJ��n�k��-��B7m�0-���/p����6OM�������
k���Ih4����������[�4F�[�����X)|_gA�F��i<�l*����g]-��gjV�_���:���W���Z���m�;�8<+�N+���J5E����^[��8�hh�?.����$�%("����;1���7bH�J�tL,����\���C��\~�����+��<��%ci(#�^��DR����>6.�� �rLz�v�����D�<���D��H�������.�QMoZ-�\Z�wc��'����F���F�He���>K�d�trx��h����U�)�spn�p#�i���xZ�d'�Q��UK��("G�P��r�>z�$F��vpk���������D�|�L�!���1��"'��I�ya�K�i$��&�*�n���$��j�t"�IM�]0��D�}Ju���ps������#����c�L�	���o�}'�IK>k7;����nT���N�b%�i�W��1H��N��i����8	��Dlt��1()o� md�k�9��=t����}�iya'�i��?��O���z0�^Ib�w��c$�����m���!:s����.6��S����%�)�z�$������U��!H�r����5H�|�Bp���;Y^�4En*�N�T~5���8���������{	�|�=�����}^��&��0@���E��y�f�]hdGH���@$����qy�� �I� hA��t�*��6��]����<��=������p��~��D�;QM�2��x�G'��u`' m�����8�Y_��J]S��Z�1�Eo�������]���2����SYGy1�k~��(��o�:��E0l�1�I5=8�:����O�)2F�1^
�oq�/1�O������Wr�T����'*xp�T���/T�MU�`�yS�1�F��t^fGs�/��W8�d���z��fj(�a��'���|r�~$����J"��9������6E;56A���cW<
<���d:����qi4^nu�7y �&07��Z�P�Y<��,�������,$�j�TP-6TPMy��������m&^��o��M����IE
����;*U�#w\{;�����;��[1x�������HW�\�u�	��)�^�}N�xb�}�r���q�&��>�dY����f�����%���:�#|C����(�GL;F���A�����$�rH6����P�v�*�}�#���i���#�nG��W���K�;�l�]���+Z�����9`P�(�?������n��%���^b��uu�I�4eJ�h��)���+/��7��q]yc�����:#~�������K�6h�4�}��S@mt�{�����*_l�x}2y��qN�����\W�b�!s~�Nc�[��m�F
�O�jjI�;���1�=�_��{$/��������_�8#]�Iy��_���]��Z�4ySiY"
�������r�$?4����p�����
�������F�v�t�����X�J ���C��s���/��)����T��_R��������l�.�}�Q��V���!=C��� �0���*�1�����%����cd���S��6�0�����92��1��:V�������/�0.L��	�;���?Z��ktHG�����e���d���F6��/���w�i�����P���)��x�-��!���O���!Xj�v�k�����(������H�W��� �-����;�V���`�Q��l:2��.�Qw��S�u��369�����M��d�r��U�
,}O}D8�"��J5�ic�cd��H~�i��J��f�R>������M�p����_|-$�^3��<�1���j����s��
~��>O���g4&L���"$�^%���Jx�W�=���%����#����/��/��A��f�6�Z
��n��t���j�T-��L5���$~���J���
�c|�}4
���
�����������9�*x���O�")�����S���kFLT���n��#��2������uE�&|E/$����<_��*���]�����i������T�i��)�_�_�����v��.��4=�����l��k�'���<��6?�t�+7	�8zCcp��J�L�y�����K���r�XZaQM{��GZk���&G\�W��������)H���%���`g�	c�Hu2@2��^�����5"-�*�O��k^�n��32*�1RGv)_�����d3����>�������O�{I!_������xAn�Y������������6�~}�V����{���?'/P�I�����7��	���`�+�M���� ��!�*
��a�'G��iG����C����N@%UN�Edr�*��)�P�����!�I��S������aD�G���D5"��T���6�?9A���&��'3tX�����8�����W���4KJ8����<�w��e�+��I���k�i��0^���>9N��4~D�'Gd���Wd}rD��Uj�Fh{tD����K�M���TN�	[��5J�ia�'7�^h3#��32}&����n`����-��X����D}�iN���3]D���������DrM�����VD������t1��S���V�yZ�Gg����	[���lRM�������2��.�����6��i��G7�Q8=�o}t������1���(����>:cm����JH���������� ��L����Y����8/����L�K�L��Gg,(a��/��X/��3�<:��b"�#�>:��|FL����� ���G��
7������q��y���k!`��G�1[���7c��s�����GG��������s�>������n`�p�����,�`������s�1}�ps}tk�������cL��[�M��AN���o��un��'���L3�>:�b�GL�����2B�+�=���B����AJ�	rq}t��h��<���,)���}{t��j������
lJ�[qN
���)��<�����^���j��c�^87��>:�����o��s#x����������2���G���^��`��c��������Qg$or�W~{tsr�:?��=:�9�=��[q7h.����o�9y"��r{���+3��z����tvH�����)�>H�^��0�����~�����S�����?���OtxG/����o?���eP�%_����$0�>�q����>u�����!J
��O_��S+��d
�d��M�$����m��^���i @C��T�T��(�H)�!����Q!�va-�����C�Ch���[R���?��Ake������Q��e[�?.K�OPV�61�[!�@{K����`�Loh����T0���:��
�U��?0eCg*����6,SU��'p�O�@eE��Tzi��I��8}`��EH�Wm�������^��]"KA8-}�~[j]���?d7RT:;gQ�6��*�	4���+S��wl��&�h��*���J��*4��aToH��^j�l��&�����m#q T����]�`Q�����S�n��w�N��U5����1"!�;*}�;i��.����T�$�
�"��*@�X���O�3�L
U4a[i��v�����] �8��	����>���K�����u#jW�:���9�>���������5���H������E������FT�m>D���� ���\PRF�P)�U��Z����FN�Os2�El�"n�Et�z��m�������U��c�o@�6j^m 3�����~����V���Z�@���YOup�r��i�I��sf��2�8�F��zr��g;��Y�(;�j�*HbV!u��Z����gu�mVG���9nf�8�6����q�����6�Tn?���Y�����:Q(S%�>]+��Q�fP(����l��"���`)�5����F0�5��� 8u��}!��`q8GbD�y��W~���~2i@bZ���q������B���Ni��A�.i���:���_Y����^���w�9�p�~op��c� >9x���Y���3e��W���eWQ�T���2n��E�JV=o���h�HB�6}'��B2�����xT#�Y�V�Z�n��~1��"���f�E"�pz�7(J�]���M��jOl�g�>?�2���G��F�`B��I+�`��������"f�E�2���*��7q�1^�����=U{s�����84e���5GR"�����b��<���U2Z c���n���!��^�� ��=�����[/Y#����%��n�$�j�� B��oj���Fv��i=�L�>j�`���?���9�X
��(�I����G��!C�|{V�Uv;���
)�.*�PS�����g'�|���uz����v���+�7�Y�����"h7i��S������r����3��&m��@��������w���3�/��H�����������]Q�������-��9w>,:�9V����������|�^{��d��7��j^f���>�����h���M�&fqm��=T��~H2g�>�����[.Hj
O�P@�+�����-E>�i(>mG�����[����o�V���%�C������V��A��}���|0C8!y��t(���i��ud����2]�*�G�zL��$:�,�����Xq�g�����	�Aa�������qD�����cP�����B����m�?���}����4�D��Ot{���[�+�h��l��7�d������t	B�!!z��X�l���P����>�~�<�����.m���y��l���*��Vq]�@�T���x��n�%��u�-H�
:dds-�>����e�������#�y=��]q
�Q��Odr%gk����^��Y�!n��b6��q��`3�ZQ��4��������4��.1���������X���}8���I?��v{����{������^��%�tR��n�F��N�@g%l�J�@j�G�����8i��������~u�1�4�:�S]������N3�����~N�T���i�����*s�DOs:e��q�y�)3Fk��������H�C�&M$�����dG�a+f��nl�j[��[)&�I���B��	���!�2��1|��=��j��_��(��:n�&cc�N)��h'��T��qXi?��.�
��(�Q��*GT:��m�K�
���
5{
T��+����2kA��S��F����h��_Z�x��x��I��}��C0��6M7#$�C0�F��=���%�m�Fe�a{B����g�P:\����r @�c�����1���x����T�r���K�O�/��Ktx ]�����e���u����q0M����1~<y�1�(�d=2ON�1�j0O�x4����S�d�@9�8�6��T�]�����Lf#�!�;�R��1}�<�
2Y���p�y�y�UH����m������F0q�Z#)����q�\��eO�`,���|�G��#(~z-������a.�qU������D������JPc[G�u��9�`� i��[�5��$����b�>�����{<{J$���(lkz��uZCvM�ku�SU��'v.���D�p�2R(G�o
�*����X�M�!��IrB��j���l��O3vi�}QW�SJI��,Yl�~x�����V=�2�2t������+��)PA�I��L$E�����gJ�=�;S�GtMiB����Fi"�V"�G��R�Ui0�����Rh`��)�g���vNB�v5���46L-R��{u��G->��'�DN��f���
M��1e5IT7,�$�6It��I���	�(JG��o���3���*�����>[�
����C�9�V�}���_����V%�5i�1�u����������#��~	�eu�>���s������2��}x�����-��5��u�#���(�S/���|3l�M���K��3W��������k��p+�e0���79�5������EB�_�{o[��]�������j��*�X��"�Y�5��P��A%D����!S�|�@���.��O���]S�MC�$��
T��������Y�������"u�S�sN�v�c�M�������H��� c{��V�e>TA,������eD��U�*��)�c����CE�bM[��-<��OE�ZD�da8�r\�d�XC\���a|y���cS������
{��r�kN3��B����T?n���b�2-����l��������
#���J;fg�rPI/:�*
\�H���W�x���IV����E��1H�7����^�c����N���Z��N��Q:���N��T��$@���ke'�����+�;��Rk���{Lf3�{>�����X\�[��][)(<��K,���2%e5CI�l�����H'���|M�Z���w����|4.�Z��^B�}V�!�O��L��7���L�O����G���|��=
D�H��*�	��")�a���������VU����r�
'��-�;�
@*��r�0�%<��V�P�[/�������d*v���B53l�E*��@�}����\�_�	��c���}p��:�*�@�-���Z�����B&?��������L������{�����sC���s��Y�Fo�u���
J'���_��s�]?:e7=��U����9���B��
B����D�nq�5&����C*���u��Lp�F3��B�j�on0P�QS�z�HM����@l��������l-���t�<�����a��T��+j\��D�Y��@�VpzGk�JZ]6�V�f�o:m�'��X�a������e;I<�R���+�PSy5]6��b��J������u!�+jYU#�!)����������
oQ��F��{��T�F��i]�>�����S.�Z��U�+��)`��	=]��v�gv��BN��+�"`�q��G�9!���������<e����(Q���5�V|$�Ve.��KU&t��,����$ES�����&����R��8�5����9�T�.��?���/i��--�t67*8RW���p0pWD��M�3M�?��C�CwV��o�)����T�e^��aW��s������Prj�V(W����XH"�"Lq{gkE
��u�ir�^�V��!���n�k'�G���!���<� �3a�.�A�����%�.�U�>+���ZV
��l���t�M��������8���	S������!8n�=��O
��W����y�tp��/*�������w�A�^�]l ��LI�c�}K�������]����S�t*��U�;M*=�*]�e�6v|?�}?��~zG�]�*���MWa��������#Q�!��UX\������=/iT=�bw)���PVs�J�DKP����qo
��[��:���s���^d7g�2�*rU��D�x�|P��;^L��v���'Z�>N����V�H��B���v��U���!��L�%���.��h�'��%��%}z
y-�/���m�����^���-u2�X�-K4���7�6��0r��Vj
��h�\PF)�K~xH�Ds����
]�2��w�`���^��
�+�-��+�*���}���N�#����n�N^�&���������`U�M�aE�r�\���fG�{��;���{��C�u285�D�Vh���[�F����!�]���V]�9G�d����DN%�p]I�P�o1�j{��q��2�k�@�0���u.�p�vQ��;�B����7G6�����M����������^��Z.D^��w�����|M��=lv��j�Xdu��������+u�+�t��]��%�A����I��j������G�S5^M�>��������i�&��Q�D�x=�}.�W1Y"9}VH�{_�SFJAC7zQ�H�����*HJ#���;��MW�J�i/�:e��<8���^�8����2�c��*5N�]Jqvf�z����g�����I<J��s�7
����1��Ir�F�r�k"I�apd+�v������*� ��&R"�������.�M���T��,�V��]��&f��P`94����
hH	�\<{UM`�����0.�
}E1%G�K�"�{b���&��I�q$S����#Y�6��z�n��u[�����XW)������V��X3F���d)��^��-%������(\h�yE�	Ed0�Am���T�")h�w�LG����3���[Y��S3��eG�7}�BP�M#p���a,�Z���0�V{G�J��):��T���l"����M�n������:,W�Nr�����#�����.b��1�tq��NS����)�$)c��_��e�����������<�����0��rX69x�+�d�rh�r�0��t���Xh�f�j�Q��)9~<5Qle[�=_av�E��\��0n8�%��e�s�����+
�m���-r�O'i�nwo����]��^�0���@��j`�X�����#7[Q�-W�w�Z�����/n�����K�����*U�<�?j�o��������*g*E��MI��!�~3{n���U��j�E�Q�|a��s�5��:D%I�O���	P����V�;��l4�m��1�~W�na��#��k�E�JZ��o-"������;��!���c��J9���],��
����1��Za�l��L��Y$�d��R���&<U����y����
���E0^����
��G� ]T%A�"������?�������l�Wg�����0�$1�~Zq�,��B�_�d�����[�e��dR�C�k�
����z%'Qy<LUPB��POy3��4��g�m��$��j*�o�*�rr�����(��)�����$1�����������[>�T�Rj�U�
�a��
��KRn;vwl�_(]�=��2Zb�eJ���"�+��PE2-@y(�u�?d�7	�f�)�o�]��PG��Pb��75"�pf�&�G��7�%E��~P �������GP7�4JRx�_��]�\��J�$�lz�v�5^���vv7�>�?�O�p���xQJ)E�� �V��p�>6�S�/�%O6��T��NW�y���K����!���T-F���L�~Y��%�,���@�S�-n��r�Ny"�S�Z�J����	�d�����uF(���S�w}B������(_B�)Pc���pE�t�TM���t��a�h_��y����
���w�y�hp�������b2�n~!<W�vz����4&B�
�y���z��_ATo����I��O���_��B��#��X��������E����$n�1����|��b+�_��b�fg�?�o������`�����Q^�o����)��:�.�l���M�)�o���Y��
=4��r��sn�g��3%M��K�A�k{l������?v�h[�����
]SX�����m`8���67�H�D�������
O���GUr�}���?}������\�w
endstream
endobj
69 0 obj
   11348
endobj
67 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
71 0 obj
<< /Type /ObjStm
   /Length 72 0 R
   /N 1
   /First 5
   /Filter /FlateDecode
>>
stream
x�37P0�������
endstream
endobj
72 0 obj
   17
endobj
75 0 obj
<< /Length 76 0 R
   /Filter /FlateDecode
>>
stream
x��}��.������f�sY��v#��qr�,^4�v�n;~����JIU�T��w��}�Z��*��(>T��_��a^�JP6�P���_���������_��Q�~����~}s*��Mi �/������_Q���������_��C��y}����?��������x��q����_~����}���Q�����A�^_�������{���P���p���7������W�I�^�����z�������x����iU�N��U���R��9w��Q�
t�	*���^+�8�b��u%j�_���~���pg&9����������&�_SJ.)��9i��6�`�`�q��I2E���S�:�����:�����hxx����5��)�cB��xxH6�P��sU�r*��i��\Wx	����9G�$M�S]Z���){�'	�!>�@L�~�_�2��&�\NKe��p����w�7�)����r{L����fd�)?��(n�Y �������+
H�C_!;D�O��!fRe�]�������(|'
y�����U��2zm��v�?�ld�U���X�V1[���������.%>��q�"Et�:���%���%����h���Pc0,0��}��J4�y�d�!�}@�O��/��	�&rc�	3Y�4�I (~�Q�f����(�S��U9�e�U�� �NB`G!��	��!|���o���F y1�%���I�j�)Im	��>Ih�L�7��pv��$����Ar�Y>,�!��
��e�C�e~��g��_�?tHE�/�[o���7�9�;
�W�`�n�mtV�������g�����(���p���Q���,�.�GO��Jx�r 2��
��em�t 5)�C�������b�'��J���Ix��%������Nt%��@fetX����N^�1�I�^��A�~!J����,��@|�#��#����YZ AkU��4��t�b<Y�u�b����C
�t����R�C�e�g
��T�k������8�#w���,!���R�\)��iu�	����(^:{
�x���o��U1?v"����vn$0V����0y�Y��v��3D%\adtl�TF�9�gqx�l"���=�����FTf��A�[�Dxy�\a6!c2b�;	���d��.���i��'R�C[��J!)<�E��DX��O�)<�R^n�&�1E�����dnDcL����^/�9,�Y6,�*0a�zg�Ac�)
�`�J%�W����@��#����-C9��fB&�t�9d2�v��/��Fe��p��3,�D�}0v(��1�����d���.^�2Ia?]�;���
���,`w<������m��;����DZ6gs�"���z���i�&��LK���
��8]N�,�1 ?x�9d����W�=������{JeU=?�>��x����CQ*�`�8O�,��'2�e��
�.n7�9e������\8���,P�c<r�}G�*�#���<c�����u���(p�R6���sT����Qq#P&����b������xRkc
R���R�nO�')�T=x`�AF,��!E |��HJ��	��(�#��	R�n9��PTAe��+]e������x�QI��i��;J��	uR�VT�!*�k%�jw�U<-��$U��L*Fl6@^R;��77����PrA�V����8�;9q�P�	7B6;(~
%(��D���	�YC�;�HXZ���l�������-��ym�s��<+!�)=�V���}��5A����f]i�P�;D�����~��~2t
�LCX'V+c����9k�Y���>�#��r�
���.��e�-�F�&��^�d�}w��70��������tV�&~�x��(�-
����Qp.��F*����c�?l��a8P�mrv�'�������n#�w����h���� �Pn�	����CT��X���X`u�l���]"��E$O����VA�#@DO����K�!�oN������c�b
H�'{�7E#�� <}y
��l�Qw�l��9"�F����<�u�mH@�-����e���_�����(?�s���	x^�P�7�%N�O@|�.���w��Ap�+��z_� 8xv��� !y�+��>��Q��
������R�J�\���r�mKe��� �'(	�SdQL��������*:��2���pZYS������V��"������'NP��d�&<�#���"GR�0��e�*����������aG��P�J���L"�N�N��3��	{Kr���Q��nPv�@��	���`�����y����y	~�����A!d�
��
^�u���fG���
jo��K�2G{�������})�>n0�0x�,��1?`�r��dl�[k%6fe,[��g�y �����-`�N�Lp7\������Kq����C����B����B,��o����"�N���_%-�����5�UctsY&|u=@���
d"��>�`R�s��7r\,~S�&/�i�y�!�<9.9)����?@����Q��.��L�I��E�m�k�N�K*��4����r#������[�,�!�	�Ys<-NR!��$��C�<0~(���4V���o2&���y�#�<��e�����_�F���A�;��i�n��|��7��y
*8��M|E�2,�����)�W�<��AE�N����	_w�^c����LI
����9	�@�>����w.t�B8}H�S�A��	���n����=W� Cx���_D�IR �=)��H��~g9YZ gT��2A8,��p�g��C�����uv���X��AvF���VQ�79����#�!��[a ~	jLC�a i2��>6��K�Gw��e�n8oT������j�1���*:����rO
� ��oa���o���(DE���\����v��@XQy��
;�<���P7�^�M�/J��������N���,3��+�`����<����������*��"�Aj�����& ��m�2����2N,�x���li�x����?�P����(~�xZ����wjS�z���x���&��+����(i�+�l�lY�U~�P1���uV;�l�v.��k���J.�>�O	��>�E�p#n�q$<� �4)b���eP*d(��<��I�d��Q��#�����Z���2�e ���g�'�n�a���^p)�����D[v\oF��n$
��)�.�$�<���A�V8<s������������:$��e��<}�~ :��S��	R�f����o�|}.��Y@&^H*������>"�����5A1�nm=�.��heh��m'@
a/x^N�i��@����B�
�|k����d�
��Z@��X1��U�&���5/A��G@:,A!9S��B>����d���jW��a]����JN�Zeb��]<<�"4n(JH��H�\�.���J|R������(��*�u���]�!		~�9��������iQ�s���������K�/��p�C7@�n���N��
����x�����;�y���gqW��#��� /���2�7��(����� �m��e���EQtp����/2�������#X7f�"C�i0�����+���!~p�1��������F��S��f��f�5S�)EK@�����L��u���eG�K�y���q�yZ�u�T��(v�3����nv�|�
C�KKh�d�2�~:�1,�lR�K��]KRj���M��wY%#H����;1A4[R(#~�:���|��x=H����?����B�����3��S!1&��'D�5����d�Wy
C��F
��S� O
�m4"l�����Y�R����M����z�$J�U-2�}�����L��u����S�<�$(/��!?�i�B�����L���q�������R������|���S:"�d9�&��Y�(���A��;��S�(O�-�
��F��~r����hx��������M��'	�<)~�>�N���gbvJ�qWB,��@A�r��s3DQ�ni��vK+Y��X�C���oZ��Af�;vpO
�C��s��l�~MR!/�A"��M�F��YC,~Ss�����QZ��N���A�9)[�BN�c"�pB`�7�*��l^^�4���A����d&��XeW%�����;k*�#]����%)m�k���Rv@^�Y��We�r�+hs^�X���o,��4�Lx��<~���W��~8�!��B'����h�A��� �^����n�$J�.SwW�<\��yJ��k&?G�����*rg_��Q!���@l�;���X��+�����
�|:t��
\�F����!`7<er��w�.�q�;��s�?��R ��x��!%k��|�Qn�|\�������D�x�+*C���b���G)�z����Y������x��������~��K+�����������c?��~��-DKy}D�R��_^�A�QY_~���o��rYW��?�������������e��L�*���$��������x�%i���P�x��WO�:�l�)������V��
D��-��CGt<���nYh0�d�����/T.�du5��<�����r|���d����Z	�^K~��EX���fS�x�`.)X��W������kTU�.�G��'GWf"����^�<���d�,Y�>���O�Hc�*:R�x�����
�?�c�U~z��d�I�0S��$cR��G��s1~��G{��V�b��=6��L�?�cKP������Xk����x��:�R6�?��-*�4c��=6xUq)�?����lQz&����F�����xt�U�a��G{lv��I���������G{lI�LV�?��U����K.O3��T��n�����&�z���G{�M�b��=������������G{l0�)�?����xo����hO7������
l8��	���Y�`f���
lP.�t��=��������Z(��gl{��[[6���<����`�9�G{����i/��X�U����G{�w*��nt�U��y<��	�v���^�G{�!+���m���dU��y<��M*��n��f�����x�����qy�s��[��~~���6)�O��h�����&�xtUpf��G[^��2c��=�~mG��dl���V}/����P�[T���`�����������~���7W�g�A��c�_5��t���bV>�����/o������/�����������~��������_�~���D�F����/H�W?����x:���9x�������?������?~��P@'��V�%�t~x7_>Bv�����?��K���������y3E���
/������uk��mDT>�#�F�b���}��ZE������K�F�@X����~��#b��rx/^y�����7�P�����O�Q�S%�I�hL�0��S0�d�?��@&���6�h�/_��U�K�����=\��h}ri��'b������
�c>�
*�d�����Z�/����_>����(��/&���8h�9���{R�r?���_O8w.��^3���X9��{3H��T�D���/���^k`��))�1<T9�4�����i���������;����t.cH��W��1��t��
n�����#�P�uN��[Pfg	c�����*8�L��)�-���H����������1�����R���@��G'�#����tw��Q�g��I�A�����Z�������6d��Zy��i�J�/�d�n�F|�d��W�~�d
r��T��b
�)r�Giw����t.�yq�l�.����m.,:)����:~����h��NJ��8,AP�-�6��EW�B`\/��L��?���dN���D�d�: ����K[���H�Z���g�y�t��g�2�V�z�3��Mk�A����T��b����tJ���J�B����t	��P�X�������)aa<l�t�/k<\��2���n���5����mGF�wk?�{w0����h;���&F�L<�����f�������i��m��#������ph:�����j�x�2�J�hg=�z�����T���y�P=	M���+����m^���N�F��\�>���6�}'�.w�����f3P�8�-����h���T[#�2�b���g�6��5��l����!��!nb�#r�4>eD��{��N�"q��Xu�\����M����R(�B�]���sX��9$���};��z��`�![�������8�[��Q�sP!�6����xx���hp*��)����^�Q����V��T��*�=�K���M�ZF��h����0:
��'e��&��s?^�Ji����a4�f����KF��2�a�.M����	�mA
�e1�����]XFS�����d`�h�d�u�c��yd��'�3n�'�<�;<�������4�gk�MR9��[��\��\B�^s����h\3��h`����0��L-������W��]	�WGI�D?e��#����;.����
���#�J�-���8���5X�H^}PC������`����|�)�v{�+�Q&�r$u$�(S�e��(���Lve���5�;`_�tC�����`�_���	_�0�f�����G�# ������@��Gb�����0�bY�����p���F���~Xn�r@o��}
:���R=����I��*X���}!AZ�����NsQi�#�
y��	�{�G����������+�����
/u	�s�S����<vN^y������?�p�9������e�vy��!�mF��GW���6��0��YWTt���0�l	����S�8v*���:<-l����l�?��/����1��_4��5���\}C� D^�p8Zv/wv��=l&�d}b��(�&&�3������Z���������mtG���I�\��#�-#9=��NO���(���U39���7����&�j���[]�
����q��:jZ�X>�/&�ZR�������R�1\���lp��(��s����8���A�ig��r<ujO�p���g��~�E9�~���M�K�Z�U���� ��0Z:s��k��*i�
6�r����H�������>�x��jd�Q�82�E��3��NFD^�0"=^b*:��b�����@��z���y7-��3!6uJ!�m_L9|.�xVp����Z�����;s.��n����7 �$hGL)��8T��Xz�U��l����n�������TZt�OYH7����O����u2�5���^�x����3������u����D���!�R�]�Z{���IEZ6EFv�e�5�P��M��"����+�"]�yT���Ng��n�H�M\i)��j�k����O��d9�V�"Yd����%�MC$���2f�?�M�3�9�,��"a�;Ye�v�3�9������@m���LdN*9���uO��Qw�Aw�.y���WB6�V�3y�tD���>�����l���`�;�^r`'������a��NF[�����<Xm�����5X
�W����ms@5K�����c�&�u�<.���s=���D	�
�����u���d�[
)��kp��g�k��}��G=cx<R�����CD���i&n�f��������[�!ET�[�y���U���"Bn����L�Xh��3�j0�uO�LR2�j�$%�����r|��d�d����>���{#1�[��Epf�6`�����%w��n}=��#"����OKzO-��/�����8mFZ���^4�c����=��Y'$��bcs�;���R���a��%�u3��%& -�k�k���S���$�i�b�?��"x|Z�h�������)=��$9>�|���s����}MK^"Q	�=�?F���2�$pARw��s$�i�V��#�N�-.?9D��*5�6H�**j39��g%���~���1�F��cb�5Y����|f�)i��F��7�S�����%6�4�(���D7����h��J8l�����JhX��A��S�j��2h �b�e��*�P����|��y��l�l��4��-�$+s �u�?A�N�L�l����J����zm�1Y����#�d2c�M;^b���$�g6�I���#
)��`���Hs?]�0"
AY����h����u(����8�`��f
��6�l{f�L,kyXo'=���~���E���GF�=�`�jlg-s9�,���?���TA/1�15�2�W���_jRBxM;�����2`4�����j������������e}MLf\l2����S�!�Y���|wN��
����0'����[v� lS�(8�Z�s�B�l���	�3���.���`O\���P���qk�~��`�61����E�U���jg�LO	
N�i��)�[���t>(��X��Z�� mxh�0��*�262�������&��t�zln�X�����W--�*�H#Ckt��
8���`
���P���N#��,��7���!�E'�!`?�M�kd���	� �U]Ko�������K�5N����y\����lYZ�;R��=,�3 G0�j@�|de@�����v���9=�@�P���3 ��u���dyA�-�g�V��A4�e��y}�VF��	L%o�pk�[V��A8���Z�6�G��]��T����dx�JMz���^u���g�kdG�kaH\3%�J���J���-�t���R��)��O%�Osh��������
���;�;�����`����vr
��z������
Ef2���c��Y�
��F��_����d�%G|6��p|�`5v��!s
��Vo�k���MP��\%p��c_j���5Q�C
[�i���7ft�^O���k�#���&58m��gZ���`2�.��^�|_�.��l���w��s�	f�3��`��\�:�=��x��r��g����I���z
���\��J0Q_T��d4�6��1�Wz�G�g����q��*m*���(��c�l��}"�1�R����w�>S�
'�ow��h=Y�M!:[����W���n��X�.��t���f��������8Wj#
���G�kf7�*3 h�3�P�w��i��"&s']`�"kv��E����)�����>r
�nR,��O]�aj��a?.���B�A9��=�xK�����U�����.���T�EF�}�
!�ws�x����u�����|�����(����|�T�����~�1@�m����_��1������5�������2"�C���!��r��[�:��R��
��^j9�C���}��	��v=�c_��>�����^��,��<gQ 4�p�5Oq�D��NJ�������t�m-J�m���2\��,����z��H?;D�S�H�h��~���e���F���������i���i,�r�1��8��{+���tnl�x�qe��r���<�v���!��9��V�^�A��NU+	�h��.�$�1xK2�B���GWS��.�H���s����<����<�����
��0�iz�Z�������-�����{�|�|����n�����L�^Iw�~ke������ET&'�����y��~H<:!���v2�|IFV� <s�f]�}�$���dO�sg���ypA�����"k�~X��
9�&L��>��E[��v����3�T�\����������b�:�A�D�K�D�x����q��7�"k5�0&	o������"K����Kd�9���,��Y�Vb���'�H����C�D*r�W���t���*�V���R_E��
i��P�0�W��b��
����.�U9�$FB��-g������FF�X0W���IX3�g$���zF�/%�qr�����Q���1�vy�%#��������S��z�����d���'�����^��x�B2j9;b[8���,QE����>a".�c�r ��0�������������L�l{F��5S�2���Kc:��4����0�HKBUSf�I�Uh:QKc-_e��[n���J-�D��)g&���z]
7\�]K�����U6wi{B+3c��0���g-����I�9��
,
7����t�)�K��?�z�YO���Q>�g��f
��i�}���^C'�U���
6bg�u����F?�on���oD�7�w�T:���\/���!}�f*��:�Z����`&
B3�.�F��F	�;u��*S�9cu�X��E&_W*��i�fg�>��z�>��@�C
!���s�h@_A���r��+^3�`q���7��)��;��-�������Q|>SiS��;�9y��r��������q;���B~�L���;���76���B%���i�O���Te��]���4��5�I�r�Ic�@w�������&d����d\�������915�����q�����X\���s	$+�a{�Zq�h�5��.Y�zJ�G:��7q���de2x�/�P�$��wP7��g����w4�I��;e+�
D�Q9�<�&��,�'b�����A���:e+�!��9[Y�R�>�[�g+?�}�������6���1�OY����{v�,���A����a0����y���E�V.ne��Gl�NN���mZ��1�����IVO=�L>6��wo�de������W������4�m�z�Ye�|�de�x�6i=��������)�k����� �� ���1��^������N\���y�L'�>�x��e��{�!}n�;sMW�
y��
3�
KmSc}<�7���~\N�V�LC����%�3M����,�zG��|����?�?�4A��������
�]��u<gF`��>�LA��/S���{(/��M%���{I�[���D����P7�y��Q��
���������>�<B���Qc�O1q�n[�X`.�M���22�c���\8���8��;.+��v8�>@t�F��1R��PZ�kq������k�����8���yc���r�q�|��:��	�}���Xu�^:N�|����{�T�."�v$q��:�� C.�
A�%9��"~��t�Z��G����YM���eT���w�z4�w�nU�_���
���
�-�gNc]`���:&]������Q��n�FC����?�j�����[�����������������,b=2c={�z�od�������`�4��J�����3��<r\�n���8�b�)m��o���W���s�^�����o(�i��c�m�v9gG��>��8u�aQ��s��O^^/^8
I�{8b��2Gl��C������.)8�G�����SA�`�����x�T�i��`99f��d�/$<�V���[�G�����q���l�b|[�Ms�@>����2��]��1>�]e
�]�0!M��a�9����~n�:��b�P�������5g�iU���Jc��!'>�n5��>���v��x��t����$�p}���$M;�g^}^�rQ����5#a�����y��Cz��i\e�R�)����zL��8��s��WC�>�Ky,I^���W�G�F�
��A���7"��.F��:�n|�{i��&T��?|a����1�4��|�=z&�W��p����I��I�a��C=*�4�������	����]v��QN��m����m[m��pJ*�N&�?%��+q������5c _b�	���c�y��C�)j����
��n>����pB�4B�����������8���W�����AB	�<�N�Jb���#qgB������=��|y� ����YDY�O�U���
��Cw]�$f�]��O����o����.
endstream
endobj
76 0 obj
   11886
endobj
74 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
78 0 obj
<< /Type /ObjStm
   /Length 79 0 R
   /N 1
   /First 5
   /Filter /FlateDecode
>>
stream
x�37W0�����0�
endstream
endobj
79 0 obj
   17
endobj
82 0 obj
<< /Length 83 0 R
   /Filter /FlateDecode
>>
stream
x��][�-�q~��b��l�T��L���y�a��m<�xlp���n%�[U�^�?xO}K�*�T�V������omT��~~�������O����������OoZ�����ed!F���I����=>������C>����o_?��������<�����~��O?���o>��M		����
����o_?���w������������}zS���_zX������p`�������?<~��7)��`�C����.�aB��0F�h��)+�5�<�I�?�H!�p����r+Y�p�������u�5��BKf���(,�>���6�Ci;OF^P�3/����b?Qc
=2����#S�3 ���f�d'_�!�s�u"��1F?o���P��76�8b����N���Aj�Y��4������j,��a]���10h���dU�1D���[�.�����!��s�Mt�������1�^'+�^�8�7n�B�+�100�$!����H��i;�i/���7�����V�����F��a�
]pJ�?F���p�-��(���A�@>Q()����f���)�sN�|b'����//w����3��A�o5ND-�����=���	�
�����2����,���������o�XB�'����8*K�5��r*�t�,O5��~��yd?�R�D���|w.��,���h�["������fg6�q�odC��~W���V��h�I�7t�_+i�5�x����s�Y8�k���J����Dv.��y��� t\�+Qy��t	j����K�)"!w��/HOO�x�.�p4��F�����6
]�m����SZ���W!�h�A��#/?v���1����1�����In#-o���
@D�"W�����D/r����+#Nz?1�_7##�y���L��,�	�cM~d�HI���m,�~�*��i��#�����h�.�������&����4\�7V�Y���%�*�0+)��#�=�7��nV��;�k\��v��/�gm3G�g�)��l.�VRE
�����S2����]���&�\�#	��Hf�K|�=q�#�w���w�C����r���I!3�k�����ul���S���O%�gu<d*���T
r�sC��<2r;Gg�0�P'�e0���P2.��QV2C�3F/���w]}�G]�_7���6/�m��N9Ug���$k�5b�
x�?�Bpf�����b&f�:�C~�L�8��)�P�����o#o94�j,��WX����-��O���X�M|�����,�;ss]����k^����I������p�KLf���L�w�d.Ax����`7��`CHk���%����b��+a��d6�Cwf	D&0������
�2���~+��N��K;��HG�������������7V3���n����'�_�VA�#x���<���RC/2����~�s�s s���
���W��d������+8���.�;�(`J����i�t��3q#5r�[�em�L<���~����\a�U���-����
Xn���-Y�u1�!II�$��S����=Gc��9�6s����S�I:;��F�K�
<Fi������,9	J���F��'}k�����#%�HgEq��~�=G++�!�	�G�V,�{+xz+j(�O���G�1nINO�*�������dz.d�J�q��x8��P�	=�Q�!E�(1���
n�C.��;]+����HPV�#r����Nt
���8��|�C;/����]E��0t-t7��S|�P@���Bw$c��H~r(>F�p������xu~d����'r��*#�'K'
YE����yB�@�S#V���4$�v����PB<����E�tv��q�Y�����$+5b�wG�n���:%�kHWm����5$ 9�F�ht#GH!M
%���x�B��y~�6���^@���D���a/E$z��R�g���`����a�����(���	��K��T�������w�
���'a�!c�YJ��"j��pf5r������z"63��i�
|�v��H5�J_<��E��k�P���F�;���pL:��;AG���*�5BQ	��_����3�����g�d��f�����l.�9X]�9�1�q�wFq+��$pO?�������&B^�p.��}i����Ej&���1Yh4��QC&�";��JX����W�G���K����-�Er�]��H  �������B��������'FF���&���x:#�0��md����Y�^O������7���L|d���!���G~�\o7�u���w�]uvRS"����j�&���9����!#�����<����NRS\K���u6_�:�|
�
�N���)"��{���C��P�@F9�;c���I��� s"�5�.�S�r�2�<���t�/z��$��V,�{+xz+j(�J�;q�F��e'��<O����K��:-!����C�`!AmbFTlVI�!]Xg#�4t��,�> Zza-��O�
���s����nld�����a&����r���:�(V~"���l[�s ��������,vZ�U�������}9�V^��sIH{�^��yO\��HV�o���z{�����C@�cW�@uMS=���H�~yD��<b��<��tA���G#p'9
r.e������<$�vv�#��)lt�f#&9�!�>�����,w�pwf���	�;0�$Hn�D0���f����C�wW�����Y�;#���9I'�^R��%��@�7�
<�)n�\��52�5;��'����
!i��(a�����(s��:r��(�n����_#riJ�E�k(cetc�N�~���dU"�(�2	�����1�|0j����O�b����I,�c�{\L�E]��+J����4�.�����G4��#.
7�#��#F�N���l�vyR�'sn����'�����G���j�b�tFd����������`WG�����O�@�S#v
d<^��u��f%�7Q���T�G�K��l�A������1����~�R�0U+��Ya����q�w���)��M_)Ha`�g����)�
=U���Q�����F������g�`"�}��+����h JrD��"�-.$[sS-U���x��~3%�cL����PI��F�b#p�
�Iv�0R[��"�[p��l�%���_7���O"��oXK��+#��y3^�`�;�M1u�"�;aadL/d�/���'+���f�������F���vp�w�y���	�9;�����0|%ze9�q�Pi������^E�F�C$=\���#p���l�� 
)�8y��k�[���<��Pd��E6Sh�b���l���9w�������F+�	D��#��`MI��d��Kr\����_���x��q��3j2j����m6 ����R�Cn(��0G�5�Z���T��������sD��|0ZXO���D���	=��1�����Bs�+S5��82g���9WgNF�ItC�P��;�C�;O���6v1�=�'��:���� �kHu�D.�O�@�#��������p�{M����i�T
B�u�=?h��V���&�g���iO��J�
��;"^��sf���9r�i!%������M���(o�N �R'�2,(��[����i!<6�F�_p������Q*�C�q�\
g��~��L�Y�<f���:O)����C;��c����E�l���2%�
������a��Nx�39�L��}d����I��8?�b��n����{a�aE�cd�(xTq��U\�|��3p����a(qlx��~I�V��e���9��E��k��~P��>�������0�wJ7��RW��x���1�E_�^H����T|5�T�����;7�Nd�s!�"e�|�����1r(2�qQrx�w_Fk��<a���A�������s�De�B*�;V���3��s�0�M
>c���8���|�-�(#~w�6�$���*f�H�
7�y4p#2+�x��������w'>��	�9s�\\�zd�w�'w��kG���p��.�so;����M��M���UJ�p����K$����M|���S5~q��W�Gn����d��Pke���������u�V����I��������CI;��&��&��8��Jhy���1HW4p��	$�^<���O��_��&����s=����_���R��?����o������yH!��)�����*��(����o�	����mx��h,8�=��������A&}��o��*�3�o��o��J��.�l���q��d�:�NFh{�����Z< ��L{���\���d�?,��������C�=��Yi��d;�UB�Yl��
aD�';�N�@m(>�A���q���:���I����������0g!��F���k�N��,��Ov��e_��7�x=n��d�a�*L�>���������O��d;g�"J����>o�4i{��*-�v�>��
BE5c�#N�h�
�H��[��/���\���w3�>�c��L��d�L_��v��G{JY����-���:%���\��^�[i��G{lPB�y�������X'RB��-���F%���\]�ZU�����G��/~5��v������h�M_����h�.`��&���h�� �I��G�y�f�w��G{�6��Ij�G{l��������D�����h?�5�����6����=���$5��=�k�>�=b�����Z ����9�z>���l��Y��=g��O��=������-O���+Lo�]�z�������mS� +��m���*/��3�<�c�g��������`&s{�_����m{���8a����h��*/`��G{�SB�y��������-�8s�$K%�`�y������RGg��-���}*%�j~��h��`�����G�5G)������F+d���>��o���7c�����f'��}����02�Y��O��JE8V���h�b��j��G��0+���
����(d����~R��s������������X��s��i�������l����U�z�R��vc4���������%8�����+3���������)��~���@?\�*�����o_��!?�C=>����S����S�
��|��[�A��U?>}����!���1^>����C/�^e
"
�!��"��*�
*���`_!e����:���T�	\��N��,� ��/��\��VY
�������!>��M�U��WA���
�y�������4�C�J��<�*u��&������������^H� �������$|Ic�K�ng^!mU��]{=�JA��1��V����HNE���`�!])c�P�-+f�7���Z�1���t/����oJ{AgW�Lj�7�\op����	[���f_np:l|����O�I�YRY
�(�<U:x���^���L��C$�6�Zv�bT��i�G�����;��!p������_��J��.j`\��i��=�����8�e�K�����P���
l�U��n�'�,���>�o����~O�����{���n;����J��$���QB;m�L�h�h�K���� �T�y4����6�-$EuF0!�
�����)��-�m��*�5G����||��w����4�?Ue�$~���a	!�nD� �xHDQRB�Z������=����� ���O3��y����X���(���:y`����a�z*5\~SSM����l�$��C���y�J��<��d�/��t��7M�@�^n��3�B���P}�z8�Fx���3B��t�/�BJ�E�|��Q_�~�������!��p'B��*�=���������t+�"E�l�4��u�E����������Lj��Ie�h��EU�N[�Z�t
1/%������P�@]J�`�k�qw���\�����a��J�OsF��u"!
0kRGQ�wR���^�O�j����������F	�<�������}�V��9n����q8�_`w_�"�KBS/\DQ�`-R�w)b���I���H�3�(p��Z���)�u�]��Y,���u
�N������"e��p0���0��;u���"��s�"{��-j�Zu�/���6*��=g�lX�RBm��c*cM�R&����du�#�h����y�jc�������?'���t��M-@����i�XE-���qR���(�M�I���P%9�%�l�*$CH�,��B0(�����i�L���D��
��v����p�P[H;m_�C1���D%�(��n����s��3:���E)��=L�J��XDQ�w�wx��l�vP�xGK^/)qu:R��R�}-��wW ��9�j�H���L��t:��9�M�6s�����~w)|7}�U~����S���K+�UN���)mF:Qi^`�y���u|�Pl�|H+%��X��1���X�{r�% ��Y�����K�lt]L�D4��i��OV��ge��PJ[�g�Q�-��2�6
Mo�I^b
e�����#ao����v6���_�@kS
���_����TQT�:4
4 ���o�a�����S}����jVD-�;Y��uVv�Y��F>Mp��#L��A�!<��Y��!�-+��y6�S-��zT�7�r+u�9��=m�XO�	F��O�Z_!]1s~mbLz�����z�)rA�3rM|����x�����}��3�S��C�����I���vO&��&`��-� �U���z���(�����t�B�b���De�����P:e::�N���O�V�tmHJ%����n�JD�B��Z�T��U9&�����%:�K�!4�Z���f��.��Z���V�fPx]�����9�v:*����8�Q���5�(tK}��us4��SneG�fG��=r
��,�>p�@�J�C�!��!��CV���������t����fJ;����������7��RP\�e�kB�I�r;E��V3���(=����Wn����>�L,����|C�9-�#��XZVP����i���R�1��������j��3b	��1�6���W�i�?��tP��������)���9��76���a�
�r��	���;;}�+����>�M�^mm�j�q��V�������-Fe qo�dZm���tik�������!�������&a�y�A��������;Rz��6ktSj�c�Zi�7���R48����U
R�Rz�:�����3�&�,Pb���J��/w�&|j:�����}���sAQ����~L�H�����@��I�\^�r����D�����5�����{;4�����
�L�5�K�V���M�����@����P��	O�������>n����D��v^y\��N����r���j"��H�PN6
��`���A��
��x��U-���q{��U	�]�9�b�������kL����&���t3d\ z�2�����c���ls�8�t��U�����pE�Y5q>�*707��N�u��K����s�e���8��)���M�q3���-�jb��+�o}sB=��KE�v�A�%�hMr�`bv����SL������_Y2��jGKYOh�A����g	E����JY};���b��6������ {v`3��\���EZO!���������zb2����q0�OToi�����J����:�p��NTG������	���s��s��W�o��E
F�
K?5�P|O��;�?��(r��+�02;kPQ7��"�l1\��Nc��`���I&m1�����*,
6*��%��|���Rj�S){�u$"�G���"��P=.g�����-�3zG.�5zrohLU�N0FD��T����t�BtT��)���3���pmN.��S���'�)�o��5�!Ftt��%�yu����%2f���9Y0e�fC�=K�/J�i�����U�=w���^�����4a��)�v@2�����:�������L��{���v\�j���nH���I��@1UZF�(�-u�U�����}����K�GF��*@m1�oEJn�1tpV����:�m\���=������bU��C$5�!|�Q����%,V���b��lw�
ON���/"2x����Br��-�����NE���^�c��#����0�X&C���M<��Bs�,�@�%����n��[?���bx��%��.yE�������H�*��#di�i��k���V��L��������Ap�TQ��+���:u4��3���.��3����+�5'��93{�3h���r�3�]��P&����yV�"��M�-��?J�S�E����;<>M
�ns��Au1�8���Z���Q����cH]����nZ�4F��#F���$U5_?@cS�f�i)������5��2������!����&��]��`�'���Xf$G��*0���9l��Y��g��+���y��!����&��	��'��HQ��d��u0�RYam~-G�<J��\���M*M�6^��"��|*�[tX+{�90h{����FK�-������+aZ�(�
S��c���79�'�E#�l���:".����;�eM�\W����Z\W=�.�r�Q�\1���(�%���	��T9e8,�hP�5��Y�Y�����X���I�=CX���2	�������:'��Z��3JV�=>�Fj�p��<*���>��}��
s:a@um��C��u1J�G�R���;Xb{S������A����E��U1�(v|��ZZ�w�0�1�	�[�$6Lu
�?��Z J
/_"e�
D���CI/�"��X	���)u�H���A\���N��������������w-5z����8�s��s,�,��Z�-���4iNe�n�~ <R@����VxI�q���Y]�����m�M'�R�07BR'�������PY�4���xX�oA|P�1�1mG
k�|���%�����`M�,�^�`w)��R^c��EY�jDH�L�>.���Yg�3��c�R���RFv������uLuj39���r��}����Y�~rVt������
��7�m)AT�b������S�e�!� Z�U�������#�&�RGv�����������t������C��@�	��H�|:I���F����WV9GO��������"�L�:/�^Q�,�PSY���e�!��*�P��J�Kz~k�k���X����Z�"(<�U��^T�0�n(�|�je����=N���9��.P��L��T:���z'0-������>��l�5�R�97��uz�$�����e(���:��:�rY������!$�A:p�2?�5�}'���BS������!�	R|��X�u;��V(oe��W���t>
]R��H�	WYWl�zz�
^'l���^9���5��6;��h��)5�����k5�U����o�����H,�R������w�M��0t�:��\�S�&c�s`�x�&���^|���:a$O7}��ao^�cVV���+�t��������&y
%<����`��&,��[s�ZY
CZ��hUe�V&^��E�]�:g������?+n5���2;d��J����������~�
��MV������
�;� ���A��{s�O��h�����'�5��;�q�����eb�����S��FW�'�����ws-��m�=v]W��b;���s9x;Y�x(����r��Y5���'��d[�Qy����t�����?���������eSw���j9���Y9HN�]����QS��%E��Qc��1	�;�K����F�>�c4_��f�C�N&=<������ ��r(������mU�v
	$�RU�1�$�&���`��<���F'����B0KB��fdyY������M��T�u��W<������T�iT���Qcbe�T���)N��#�$�r�kN������T������K�<v��6(l�rQ#��2�2�bX9����`�1�!���"�6(5�Oq���� O�>�56
����Sm_J}u6�i/���<sJu4�iI7��:����d�=�i�����9�� �v�����~��!�cc��*�:����)�m�z�6t��������-��yC���J"�d�G�7*�[z}c�Ky��P%�L��u������5�%oV��f��f��7���4�@:L`'Y���zM��}8*�C������[�*�5<���r���-K!oy��*<]P���*��-����]k.�����~��104���=��>�������6�o�^!�Gd;�SM�}������sRamp��� ����-����F}%-��Z�����O��K��T�����k[�JH{��u�iq[1DsO���N���#�"�*q*t��!�P�e�����ou�����e�����&�����|�����+�����`�u�V��:����A���m�����t��?_/[�nP/cME9h�.��� ��K7�����g����#�S��C������)-F�����I_o���/�C+_}+?�����J6-�K�7�_lJ9;|p��h��/������:���l*9�.:f����JW
��v��<jy]����O�#�s�����-@�����h	�O�4�j@����C�c����a��/���L�`<#�G@�.H&<]���Ly�b���Z�������W�R�`c�v�)�����=�a��(qI��p��c)��	�xS�����`�s/���9Z��TB��l���%��k�����Vw��K����-�^+V+���f��C��s���>���	g��EDFo�26�Qk���nUN�$�c��4u����!0�\u���-u��J��c��������$@Q��v���<�~j�<L�!E&Y��!�����3��i;W�>����Z��R�W4�ZnH��b��s��#�k�P�\7�!?���l>t����g���S��"�#!V��y��z[G��4�w� R@O�,�A��D��N/(15�"� P���������� 4�>����?�)�7���w�`��-�"��v$*���oZI�j-.S�����i��[���_V�w�}>�}�[�XI���5�Y��R�z��Q(,���������
�S0�w���+�����g�;��:��[�a���k���}����\j��q�	�V�^�P�>��������[H�;q�K���\m����[Cw7��Nx���	Q!Z�^J��/�/�2�G�'�cP�t��������4#����c
d��Q�������IQ��V��3F^
�38�'5�lRCc������G��Sd�}'�83�*G�Q�,��:+����>.v0�2��*�W����^L��N���������|
�`�5�e��L���?�5�]dZ9���f������� �iW�C������iU���-6Ky��Ms����l��>	i��}�
������
|��!�T�HU�|UZ�R^���i<�i��JB���D��Q
���p���w��y����5>�j|�;���2��pOtZ���G��e���u��]���\��-�	�W=Ow��
��Z���N%:�ImM��
^������3��a(J���FJ���DZy�0v^F�����9���;J'�`�
���H��]�>��k������h�����-m[*c>��w%��oc����!�(:t�=������;�����7������g�K���Dp��<Y�����)����BW�b��6�BW4n����?hFR�������P���s�e��T�!?��g��l����|�P+���%LUq5��j�n�����ni��C����!T�uE:8�t��J^
N���x��
endstream
endobj
83 0 obj
   11928
endobj
81 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
85 0 obj
<< /Type /ObjStm
   /Length 86 0 R
   /N 1
   /First 5
   /Filter /FlateDecode
>>
stream
x��0Q0�����!�
endstream
endobj
86 0 obj
   17
endobj
89 0 obj
<< /Length 90 0 R
   /Filter /FlateDecode
>>
stream
x��}K�m�q����=1�o�f��i� ����;<d[�;�b ���G���U\��h���HV��&<�C?>��>{e��?����>t�_�������������b�	r�9�T��G�ZEm�������>��~����>~x���?���/���|��~�����_~��/����P���'�l���������b�?��������?~���V��8~��������Gp*D�0NY�p*������?����:��q�sp��������U�N��?��i���{j�����
x�Z%�>tp�F/�SN��2;b�1@���B��%g�r^'����6*���!r�.�3���&\ZY:�y%gr^�O|�����7~To�8l��hd{@n��������q�ia���1�,>$kW������n?Cb�LB����S6E��:�����e,n^
;�8'�r�����P	��.����0sN�G�S����������yv^�=�C�Y$�Z`QV�i��'���������#�Y����Y��u�C�"��D��h^ktqL��S����
�E$k��}9�@�d�R��F�K'���<�����{�E����3�<�,�����Y��:�;w������Vb���+�sC��}%qg�?'v���lU���-:������N�x��/�`{v^����h����y���s�UUwz~+�:���>�2��-��r$BT�2�����sF{<��=�C����s���M�]E3�M�"���p<�!xm�*o��^�<y+Y�h���M�������K���`G�d��vc��UK�����6�����8u�-}���Y8"�%@�����]��mpD&�h������7�3�g������c>L`��o��9G��?���������&�;��q>$�h����O0�#7N1�^�����D q��X8����&�!
�B\1F��l��n���-.&���m|W��4:��(Bg�0�����n��3�O��k�$�d����t��
����ao]�����`����A������������n�z:�Dl}��g�W���@W�/�7��f�C��?�O�BI�/��6�����p����7���{��<E6������q�m~K��Y�s�(�����9�*������,X���#�' o�����g���`NT����H!��1I����n��G������:X�l�3����������i�q�F�����zE�,p�%��&���l@|��%�HK���9L�7������I�J^go���x���q��;�`\P90:�<c�]�������:��� �K��$h����t��A�	x2�I
f�$�J���w����+G��jA���]���%�$[
2P�v��H���'���t���N\e�o�3����u�"|UD�#.��P�9�J:&+��5��N���I�{�_��)����U�Q,��
�����;��X>@���������e����hi��y���+ce�_�
��\���K�N�
?�� �8��f�h��h����������<�� BV(�2�����w^�~g�|=lWb�c2I��z��9�*���<��wI5���o=�,<��G����9�s�:)V%$O�*��q]�
�i����o���}�c���c�f�*Y��F&����9�B?fn
G<S������������s�����2*J$<��3Z�����c<��n��~9��9�<�3F���ct-@w!�9�'R0U���/�d����8��C(+� V�p��cEZ-L������0�z�NN=j����r����3#<�P�"�{�U>����<�8 ��.���x�����2��r5�i����0��b|���<�H��pv��6����d��8�F#����S���e���*���Ty��v��|�iD�k[�'	y�<��T�����>�	xa��PZD<;E\��;$���&�Ij���\#fe�6�m�7�%�#f������
&*E��_�����^���gM���	~<m"9�b?c�
�).��9��,����+�9�(�=������#��b�������r�K����9�x���Ee�����N��5�0O�F�g����	}#x)^�M����%�?�O�S������L��n\�z�p=����z��F+l�qxpb��.M�4��W�|�NSo���X�!���Q����������]����x����?7q�M,�V����������75p'M�_�b�U�)�N;���,jaX������!�	��c�o�d���nFL����S|&����<��l�w�����Z�V�D�>1�����4p����2����t:���gJ
[�#D�e�d�7����qEW����v%�=����h#�wA�D����h����h53�$U�A�2G��X�C{L�*fXy��=&,l.�����wf�&�IXc���)����V|F�nH�r�U�G��^2�19��.�Q�y�G�H����/�]�*��ct�-]�2�0
^�[c�#ws#��Ku���a�3Tg�a9Ws������m�=4�p�E���9���\�A_�c�
�A���D��Wi���"~v���Z"�������v Y��S"���[y��'LL:�C�����x:U 8�3�*b�����
���L�����Vb'����0*���&X���������96�'�?�SX�c�l��a�t!���kJT�V�C�6��I���(t��B:�c��1<�{����c��� �aD�c�~����'*���q�xgO���=6�v^G$f�@�"z�����f{�f���3&
�0�TD��Q�Z�@|��'I*�Qzq��+����` ��$4y�=�)�����G�����QW����w����5�c)�M|1��m���:o���C�����1X1��������Xqi���,�<!���G<
����J~�jI�Y��r��7�y��;x�?���J�Q��9��Fa���e����`������oT*;���|7r��������aq|����u~��o��=7!�[�~:��Y�x;1x��iU�u��{��qQ���U��O���Wv���cK,�Q#�����������)4���?f���_V&1��F�,��n�#y������P+�)J������	�������S���|��-���5��tS�)�����Iya|���M|��w���c����;�$�jJC���u~I�~�4U1����f�U��d��� ��S��@������yCA�S[Y�!T#,er;�|{�}�oav&��4�P�@L��o�F�����Vo;n�sg��8���y%��\=�?���u`�����S��O
<���������4��c(��U��y��Tq��j�����w'LVi������J��a(�����4��,<LG���1�Ks��#�W�c���O�~���7J8�[�Dq����dZ1�7�R��Ml/�?����w�=R3�80�|���$��U�JsX�1F�e��J?	�*v�1��j?f�����<b/Cj?f����H�H�o�x�s/@zI�-R�����5����|1T=��;O���)_�I��=	sg(��0����n5�3u�W�	�)����S��hm��>��w�����h������=`�B�~�r~|��O��<z�7���������_I���|\���h�����~�
�q-��@�U0�����5��-GhLJ{?Ac0J��t	i*��c����F�}����P<;,��rV'ho9B=(H3�5��+����-'��FE�&(���.�r�x@�e�N�cw�20��Z^�"��h_��P�s��X��P�����2p����YYk���Z�mW�9�dk9"�ZXfho:c�UXZ2c[���f�`[�
l,�a��&IE��*�[���<n*�ZZ���K��Mgle�:��t�fP����$)hC��^eX�������H�����M7�^���MGeQ^���A�M�q�+����Mg��������6�l^�����Y�l��t�o������������_����X���Ej����5>+�.���G
Ve���5��&�_�����I���p�%��zr{�
lR�Q[��e�t\�Mo:�������-Gd�������3��z�qo���
Hlm:c�)��mM�}lM,�[���ZP���mM7�A��E��(/�D��u���<�����������Y������P:-r�7��|qUlm:c#����mM��&��snM�q�W6�snM�q�V��snM�k��k��+����M�9����[��X��JzYoo:�K�BX���������
mM�C.��[��X��YO~o:���
--GY�,��+�5�G�I%���5�W��
a]mk:s�%�W����s�V����5����26���t�3&b����t7D�����t^o�*�u����MF�Uf����ST6�snM�q�Q�-2�7��F��/k/��t�����_����s��n�����6�?��`k���^3�	��(�t���_?�.�_~~����2;a`?F��L6z�!)�!���������S�<������_��o�?������_>����/�>���_~��w�$k����?|��������{�S	j��t���{Ee��O�\z�
���=c��^o�@�L�e��3���\�|�:�$"|��)�'x����m�����2iL��&��`�ozo�Ki��7�S|��Z��S$Z�
}=��|�t6�g6���'G_VG��Z?����`+S1f�	��]��g�(l�JfO��\���F�i�$x�`"[���TYA`��\4��k��tJ���	��	�u�7�: ��<:� �Q.$�5Q:�J/]� �3�-�~x����X��\�����m!5B��C��tl�J�n�h�3{�����M�2���y���3�q����-�����6�����I9K;�A��1�������,�`�
��9�s[:���c�-��?l����%��B�&S�����;�j&(o��*��;��f15��l$�]gcL�N��)cP�0A��@$q�jB��Wt
��a������^�"�z�	����"�>�x�eL']dHh[�9���mk�x
�mm��^�8���p6��$m\44~
I�����������������k��k�o��@�:���y	�QT[0�5H�\��P�X@���d�OH.2B��F�"�!8��=���>�5��!6xV�O����*�iL�Kl�$6��M�����m����hYT���c�de���vRKi����^Hf
����9��	�&#t&��2�C@S�����rK*�R����$���Kng�@�`[hT7���bW�k���bX8��R!��f\�!������
��w�Zz��L<zLj��3�5���koV�����|�c7.�������)�'������������`�����(95�m�2��v����U���a��$^��'�m����ni'�:��t����b������#�� �	l��v	�Y+����t������[�$���(Hu����	7L����&
����L�������������lM���:���l�A(��t#91cc3��["�j!q2�_��4�
����D��q�F�Q'2g��#Zi�Z����E(�&�E��V�U��V4������m��������Id��^?B�����������b�}?9;�f�H�������*{���Gw;�0���v/{;�h�fk�.������}F��E�B������@�f���t��`�Sn�_
`#�C��4���~���R<����J~���}�+����*��������^H�U�(K�4�s/����l����b*jt9f%�F?V	<�WN��>�w���&:���YRnKCIX��n_0��vNy;�G��Ts�/_0���A���s�M���`�Mn	�m�s�����o�����gi-|]t8C�wCg���T�c	�8��sfl�T�X���"��C:��R3�q�t�����"kA� 8^)=��x2]nR:!�a��n(:]vq1
"�a�k�~�)
}���-�l/9�B��9��1����l�Zl+8R~�����p���[ qx��������;3��I��@����� ���y��T��LE�G?��@���`b��2I�D�EbL��/�K��:��7�5��S����&�����,��c<�b"Els�L�M<��7WzS��f����L�{��|e�Kw��O��1��R�4j�4�\����8c�L���������Q:�OC&���0�vP��x���83��apb�j�u��~��l�\���m�"�W���hn�����%�V����@��F6c�z-��2���Q���a��I5�J(���
�;�
|����s�a+�I,���	�]���fk��}X�l,�g�90��J��a��q��S�O�����b��D��<�~�m����wlFX���\�!Q���8U�K������Q\����0��v��L��D�9F20b3���<9cX��m��Ir��1�����;�1����
I����<����s���P�.[�'e:=���"D?��[��L@p,��&��2��$o�
��*��	'>�(>h�gB�z8�j����^~X6������Y��e�Y������CT�kog��B�[����a������������}�G!�`S�a��Q��G
�\_eB[7�/us&S��m���I{t?,y����C��e�����d������#����h�&F���	�P��%�c5�>�)�y���St�o���\]�te�0H3����&&��	���l�QS�(�t5��Zp���x������g���U�o�"�Ci��{R$�U�I�O)�Yi��JJ�����"�tl�ZU��m������}���u���O��96X9����������Pe!�^)�v�(:f�~�����2=���V�<����
@S��e��?F�p��5���o���{������SM��������&a�M^�%w����c}3_�(��k�c������l#��\���,:_�Y���R|�����Bg���N	��@�g|�l��7��b���+�:�����y|kj�:S�J��oV���$$c[9|�j��%V���R���M�u���7{���(Eq*�n/|#��R|����Et��!%6?������H��y������6�>
�n�M��ek�M����b����C{�x&+��T�%�I��H�Qe���P���$���v�D���5���{r�i�����$I���$���$H�I�#7�)P�~�����l�R�r�sp���������0{h�NZ����u�CLd d��NZ��LxY�-���w��aC|�]6��J!����B����j�x����2S�����b�kj��YlIJ+�/��N�J���d�V=�q9G���O�'��xi.�;3������������������ 6
@;\-���5j�BW��TF���P(*�����4up�ur��.���9jE��=�x-Z�y��:=6��i�������<B��_j�s�����W�%�~q����:�7�O���\�����h�d�>V�`�%��USa�o(#��(i#�������b\��
�P��y�x:x=����H��,a/>3��7��^����f
([
&]�/-�<C�F�1`S��BLz�]�`���u cPR�>O��w��
�5���l� �H�0�#s����m�A����_o��W ���l|�����
���Pu�����!�}H����T<��)[�w"�E��6%�9��/���nik������Q��o�N�S�)(�������jc/F����A��l�aOp1q��Q"c�%�C���f$M��e�1q�a>f&���c.��jFZ�q���������4��H�55�Gq�D��w
7V�����+C�@+h��B�^X3}C{%���jC��B��������/�M-_�^mf��.����8n[��������rwL��Z/]��R�U}�����H�<����*(��MD�����]���L�U��f�U�y��NfJ\�vw�l�'*�$���W��"}k�������Q5s�������R��Z��y�=�����Zt��f� ���� �]����qQ�����<�����E6�=��V!�[Bt�t�3�h�|�j�(���J}������������g:3e������!L�����g���J��F���U|�.���K��@>t`���?�%���V��o�������|��{�3#��4������.�r��L�0�#�W����t�����
z�Y_��H��&�PJ0,���%�����0��s����9�~�������t�=Hn�]�����$�ky���w��������v��Zy��l���I�2����`������`�{���]�U!PG�5�q�y��N��*��"M��J�`0�����b��{���<�F&O�������'6���f���:�8�,���{��������>��A0����Qz����P+/Z,�3J�D�{��s��
GH&
g��P���
�M�4�����"�
\������#i�������3���J���s"��B��{��Aj����Kq���~]>Z��%��|��������G����>�OG���O|��N�%1aD���F��m��z��N���7�L��R��Ss���Ye?���X;���n(�]��JdmVF����X��S81�)����n
�T'�R�:L��:	���2����e
1�ILL�=����=b���eL2=,y�8��
B���>�GJi����W����a3���s��jh[���b1x�p��o�_���C����^�=���g1�����
{�c�F����0L��g��J"*�O0�\4
p�B�3�"�^y��>D��J���0|��B@������q������Rr�u6���?���}��O?���'x�_/z��s�-��z];��b(�o�\#-:�i��D���Q}*�Tc�F���*��+Hv��:/c�u�d���4�L�����n���j���6��i0�~�A���&qC�<z� #��u�n����z�����Zc��������^�P7����3{q#l$����p[_3=/��Faa/_��S���;��H0W�2��2��������tFr������8C?pU
��%��J��a[\
?9U0��V#t�g�(�})[d�Z����+xa�����v�"Co��U�����;���U��t^��b�Z��5��%@%\�����d�h�5���w!�I�/����t�cs�c�"/�mf���hC�=�����=���N"�
���o.hO1�k�4u5��$���]���d���[f��$\0%�����&r�j�����^�hUf_���!�$��;U��FR+�`���������5����L�b��4Y�����k�qv�������Y ���!&6�v'V>%l�^]bm�����(�L-���Mb���u�`��H�`_wIM�L{�&��>�X��6�GCx�z����^���W9\�����`M����R����	&�[��`r}���6�������������\�����a��w%����v�m���	`l���'<���"�B�b��,�j��HX.t��j�/������9���	��o��)���	����	��]���:�mUe=���'>=�'1��D3b��#j�w:�1�;J�A P�(��gt�d����a��TY�(P�m5����3j���h�jf*xy�����������\5��{�eA�M�o9�WS��������c���� <Z6�l�\���s}����Z������kJ�*
��r��������\�}�d2U�.����S7rkzz������`9��&�{�;�5U�
\�]���]�)����X��(������sm�g�������r��0�ko�_����$�K�������_"t��!.�D��`����_����i��a[���O�������=b���+��@A�(���5P���9��v��hF��]��{��4!X�c�l��SM��|F��m����z{�������T��.p�k���)��5�|���SsOc�r�M�����h�j<�]�<�7"&�1��sr��Lg7��&��5:w����s��L����w�����(�>L���p����1��&c�#Il]t����$1�P(#�y3�R�F�+>U ��/������dBP�k-�]�FP���'>�m\'.���?������ic6���|�auN5&���C��`�����.��K������n��"�l�'��"=��%��8F�)vB�Ty��D�b'����K&tbT�(UCXFCD!�����`��������a�
��Q��n]��������.q�y�E<4��oY�������DeHG9/���y�
J�^�����w���*���R=W!���_x�����������m
endstream
endobj
90 0 obj
   10591
endobj
88 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
92 0 obj
<< /Type /ObjStm
   /Length 93 0 R
   /N 1
   /First 5
   /Filter /FlateDecode
>>
stream
x��4T0������
endstream
endobj
93 0 obj
   17
endobj
96 0 obj
<< /Length 97 0 R
   /Filter /FlateDecode
>>
stream
x��]I�l����_�C����<l
������^<jQ�dW�]ep��o4�B�(B����E�����"B���T����������������7Y���������}���7#BJQ�
�YEL���Ri���O?�}�������������������������y7�����o�������)!�{|(�������������<�����y���w���C��Ni|�����A(c>	��C[a�������?�K�y�?����
*F-��������r��_�i��^oFHZY=>d���G��t��z�9|��Gv��a���_��WLF23$���
V��p>`�u2�����u�7��=;S�y�^��8@�a��*UDF������0k���G���8�&@g��!�5Ih��t��?�
�2�(81,���v�X�Na��;S�S6���$< �g6v�lG���V(����Uac�;l�g�7���F��gy
��I��8z��o6���'�E��"uY�*��bQ�������	��:H����I�c3&�|�8���a	��XB���f�3����6��N�7$n=,��E���=;���t�-�-����D:Op�����`iB�C`Ey��9	4�:���S�3����������9���6����|�wX�����(j�o��������q���J~(Fh/9|���8k@+���W���9�7�6;#9��Q�D�RFp[9<�o��~�V�+����d�G�������A�lH����i9sP��
��s������a�|�z���B"T�si;`�9s�A}��vJ��~[�����N�O8�uP�����[:
���y���k+���v��;�*e�$�Z��	�N�Nu/]��YyF��Y���A�r(��|4i�$~p�s��
�!{GpB���!�����~b�.,�p�'qG=��@�.�4�2A��5��x8�yz�������������/��IF{d
�xps@�����'�%�9���2A8�(`Vf|����=�O����S=�����d�F���Y���6s1pg$q��!��V�"��I���6��2�Yy1xt~t{��h�a�f�R��Co;o}���/��X���;��$���pW��7��~�Ahg_Pw$p���������"��b��������'�o0w��d��sZ(�������G�1�#���Y��L�c����c��C�9�
�Uq:M�JfJ�M��EA�58�E��V?;��S4����]K�Y�)Rx-w{���m#n�Wbq^��_��H�E�����6s�9H�Gd`��c���.�2B5�Sy����3f�^
����������B1~���I��!*(��yA�1���#������t�G�*�('70��a�n��&5���"��N���#xX�����"�n�~`���
�m�8����9?
� �JhG�N�9$��
d>��������^�9�pW"H�O��RD/�2/l �Z��j�`�=��G�PW�;�^=��*�������1�����#{������Gg�cS�����A��
n��������]�<9
Y���I�Z��b���]Ev�b&�����dsPWB�����U�'ZJ���4y�e55��C��34�d��];��/c��
�#���x���6p��8_�6Z:�,Qe�����`7��������G� \�+)�`���p,���X����FZ
xD��K���j��G��GqC]8�6k������Fb�n�qc1��`���)dFR�k�n�#���U�d838�R�Q���s�$�	J�+��JK����%`	��1[������?FN�����ww���
��&	������$�r�i+R������^��������:8�����w�?��;�H2��D47��.P�k9Cn�����*8.lg1����7�f��&��X�[�\a����c;n�������
�?L��^!�m���$��qCe�����s4���eFE���p�^�������w����v��9�
|r W���xwd�����(��8fW�����z�+U�H�W�J�y����i�P�u���
�������Dg$�������O�b���
0�]#s�������c���)���h�vV(�j�No�t� <r��
��g��t�l�O���s�6�=�:�+Q��/F���E�xB�N,!z��EK��#�����Q������m�����FD�Z�Qg�8
N��:�h��q���
�F�Y��;�!�1c���Q�f�!�q�I��7��3��-c��(<Y���F�w:������J���P�>�������s���0OBVB�
 A�2�	���);g��������-���E�wQ{x.��mF$o'��d���������*��f�1F�4Q?_�
*�[���4|=.��������0�$R;�H��ox���_�L����%*m��OE�:�lQ�`;��i ��<f����8�Z�3f�-xU.���VE�g��O�51�)��0-1�ho�.��q����f|����G�l���"���^6ibk�����m�`��Hl9��x���JH�t�m����:�60���m��u���`�s���*�+k�]c���:x���~���,P��	v{@p�T��\�v�WH�7�q�|��Z]-���@�n����U��3��8]2�
<T��;s��~����T�����
^��<�������e;WI������7��YG��Yyzm��5�	x��������Z��C�Q;���Q"����?��
�d}��+���s������\l���8��lc�E����E�F����aZ.D����"����c�p�F�d����I�t����.,~!�`�u�2?Z��:����H����V<?k�����,v}>�+[�F1�.��FMj�"��]g7���.n(6�0w&�fz�l�HB�x �I��
����bfR���J��p�Gf�.��[\gkT).�9N6�I
e�j` �u��h���������F~�q.��2�N���;��&%e���\
O�\xt{�1����gw����1������f>�#l�AIF�:_h������s�`��x�:o<�0�����:fA�D%�#og�:�'$~"��G�ps�p������;J�
��l�b��(��A��@�
e����w�h�P:�=L�#{�o���2Fg��1�3��v��}�|��\�zp)	)��:}�p�#���,n���i|��E/*6~��B����A���$����:cD�����]7/;u����1]y����� m�O��"��,������<������Ev�xXq�+�l?U`��q��I����#g�N��{�G,�Ek����G��������
��c�x����.w~�C�e��E��:����\%�)8Q�0HOd�X+�pL}�r���9������:�xyr���H��;���Z!�C�|7�
���|�����m�6H�����2T��`�k
Z�~%�=�o
�.�d��pX8���/r�M��F��3�����M��u�f�����>{���Nh�o�I�B�~q����o�		�=Ny	�+�s�g���a�==�����l
B*���.9,��C�+��kF�����~�G���j�V��;�9�2h`���c3Q����j�������>��&�S�?�}�:�w��)����_j�<�������)=>����O����/�(���=�#'��d����;�RF��a��3PI��2��a�Ix��
-[�3���������![�����^�8d.��������T�"#�[24�G��P#[���Icdo�AM>��EPh�A�O���B�E�c���
�Z������4��<�Zn�%x��~���q�.#oDUd=���a��-��e����aB��-2&!m����E�(B[sC��-2�z�1C{�l*�[��X���4Bk���t����a�Rk��T�������E.@�����&'��������Q�X��.`�03�{�k��f"so�N�`3���$S�i?��"D7ck�����a{:��%Z����3�3H��.`�Hf����(��q����5�X{�flm�cU��c[���F�������(��D��t�E�����������v_o��k�pr^ok�����c[�~\����z[�~\����z[�~\��99ak�^�sUQ������'��3�6���pj^ok���0a^ok�����z^ok�����q^ok��sJ9�5ck�v�\������]�UV85��7]����N����j+��7z�V&K�3���������mM�[#��g��.`�p���M{�3��y���sFm�Sr�\�8ck��V��������}���mM�q�q����6������TE-���{�~����z[��VIm������)��s���i�uR��������r��_����i�^���a�so��Y����6������'�M�q[d�F�c��`G��yCX4�>P��W�/?>~�����"��m�2�W&^��D��QX�|�O?�}������������}�p���|�����T�v�/�=>}��TV��Oz�R(�lm��TY`R|�01����=���l4"��eZ
�m�
����m��uF�`*�a6�.:��w������7e�#��6�vw98��������F>~���^�2RH����`���Z\���?���~���?�����~�F�1��!�������T���~�����=���*��P���������%�����j�������6>���}4B���)%�a!�.�?��	����1o2��v����Y�f=��=}6
`�n��������M��J�(�oZH9:���X�3%���e����N'��,n��(m=E���,tP���`���u<SZZ��&�YDP�)��(�J�N�@SZ/(��P��:�)�����Z��g
"��-0�z!���
J?U$��EB$��T�����3���8� �]�&�$�BN�oWf��Re]u�L��iz�T����2��Qx#����$�J�Z�PT')���������=E���`A>��DSQ=U4	&*�
0�h�*�
2��;dZ
��_�O��Q	]��nb�%��F��<���:Y���,�,�>cq�\R�HD��D�gR[P��
z�
���{���(h�P�TCo�xCAkuSA��SA7�vI$C�
�������0ur��%f>�*8�B��oB������:���y����,��K�ga
/l4���FD+��G�r^���9��kd�ID��K��P���
�e�C�T�������{a%._&t*���H0�{�]��c���A������;�%)�������
5!��U�Z%��v��n���;���n�M���0��t+�2���fN��I�CskR+��j��t u�VB�j��$�H��:`A�`E���aM�,`��i@��#�Nf�B�x( e^V#�H��1�����ZxG�-�|j��9b!�Z(5��F�V#�Db�����f{��$W
�,,�
D;�����1a�n�����Odk��������x�9A�a�Og�`�-y����?0�Q��<Z����������h�J������v�i�
k��d1��V��t�`Y)�}o���z��5c���B��2T}b�h�o�h�����~:��g>��W��|/��1p���g�8I[5{����N��X�ll�N���|���,`��hl��y�J32
A�����:�v0�~0/"ma��U���X��Rbl��3�^�����E��7��o�coa��6�m�����L� S@/];Y��E���V�e� �&�e�Q�������b�e���G��8�.�K�f�a�-M�~�W7l=��bk��jW���UQmuc���������[�k��.L���HYY���<��<)���]5�J@mP��sH*rfSH��eH��cHOCP� M4�G�l0�:����v]����wQ[����&�]���pM��n���E�1�B�^x�;�=��s��2�5�OBO�h�Y]��j���qI��@������vB{68�O1���k���*n�l,Ah��3+Q��r���Ln}8�h7|k�}>�\�L��C����:K�srP+ON�F�.;�����t��&���-
�i��x{kC��6����;Z��t�v�H�������mT���V$&����5:�i�Y��\��\�NG���3ct���I�����	
�� G��1f(�V��1���1
`��H���D9o�����A}������,�s��h��P��e�q�es�}P��Ib�9?�MQ��0�X�F�`��^z*��>�p���G�����F���i1Gp|5����W��,:�t��e�s��|���Q�C&&�
;�����>W>�#��f����a����I�L�t�Q���\#��.��C��-��dM6����)Z�����9�(�x!��e)������z&#|��]r�F��VM�+���=Y�v@)H����#LX�V��z
��H����N����^����}"U��'E��usT$:�I��U$�Pj�!|�����T���i�h Tn��d���4dt���#�$�t2�l��sb�=P5\~JC���
�-I�u<����}29��)�!x�d!Sq%Aw����l��qHC����/��Z�8W��S�����*�nfn��!�T������5&����K�L�qIUJT!M�+��He"�)�=���4������cQO�<���P��>Hu!�/(���"q�+q�I�R�u[�n����:Y�Ii���cRA��R'�����	:$#��d$�\P^R��'#��Q�*��"�EJ�@>1��U�P�r�P�����6�!,����d�[%#��)y��#er�JL�(g��\d@nJ,�:��+��=0�o���5�����C.2�������|��;'��A��S��w�7�� iG��h���.k���5����A��W�;X
��:E�����M	��V*E���D���ZU���O�"I����1_F{;;\JE���������������P
%%�#P�@�,L�l��$|<����A��BT�j	UK*w����]R��V7?����������p�K�qWk��:�))I:�s��-G2L��Y����,��"@��
/�a����;4��@;$��h�m�x5g���_;	����>5ic6�����Q��h%$mh�k��Qk�4RX�VuS$����r6DC�bcJ���G�G�o�-��q0R��C��C�H����6CYe;���i��q�".�?K����veA�6�������rre�����D�*�r��h�^s
[t�i�+�|}U�F����g�G����'�V#�m�b�5|����oj����-"S�G����\Wx�A�����>SY�_��DM����Ta����|�2�VQ-�.�!��6��a��]I#&CDY��NP�����-(9G��!g�k��r���5�!����W��R��m�j���-�!'��k�X&_#�M�H���KE����^R� �K�mwr��gb�c~4���W$G-Ry4Y����E��u�>�|��w�� �8Q�R��K/��6k�E,L4��_�as��+���D�D&�>Z�������tj�n������oK������=������ �B^{�@����@���D���b"��;��_�\�.�~u�v������5�-j.�W��s�S�,'���^8��F�i�j���+@�
Y;n��'CN7��;K�(��LM�d�������`�D�2���5��4X?����nJ�.���M��^sW#Z��x��^cB?cP�07���O�P�?_���[����E�Y����T��]�����j,�nl5�1U��$��U���t�@����{Z_�d�U|������)�gI�$��T����i���9Z'	}�Z��
]uD�c����S��U%�U���6k�98W���rY�;rIX;L��OA=)}��V5F�i�Y�	8C���S��#�}�[��E
�_��m����D�X*	���'���d_O���xd���M��r5�k^�`p6��eZOF�C��[`L��`���j�
�:��wV�uT
�J1���Qi���m8l�l��MO����Y9�IcP2v���*���o������B����dBA2T�Qo�� �:�k��A�^7��������J�L�;|�"]Z@�h�(G�m�$���89�fy��d39����_����jN�W��<kJ���9Va�����%�E��->-2�O�<��2���?dt1;�t��E�RBwW(;kz�W��fST�Jh��>W��!����&o��9������Z
>�M���*�H����*�9(��L����4���D;���l'�_N�)�}g��
���(1a���tT�Ff���!�$,_�jTy�J�v��r;��5���0L�B�,OD\"�w?4z����<�3R�(J!���O�DZ\��Q�P	������nmE�Hl���X��	�'���BxF�����ItG����h=����*�4�o^�=9M?5�t)O����I�`�n��Y
���I��w��0��	�����P��i���+�����v��>o��zS9�y/]�������u��"J�[B�;(���L��j5�7��8�26��F���V��g�����0�����2vt{W�D��Ms�c��.[/�a�H�c�g6��������;G���r�
5D���u�M�������9����4����
�\hxL���Q��c
5���a3L��u��~u�������C����9n�V����;X{���Nh������J#���`z�?�4O4q�|���v��
���Py�������+tSo0��\z��dc�^m�R�2�d����\�OtI��X.LLVs?�������$�=L�&�iN������L�X��B^P�3�l=�{^�L�dSK�2FU��\w�w4,��bW�A�Z�T�(���k��S!G�!�f�;'-�}.u����\SW���%��u���S�K
t���%�fl���o#��
h���U�7�	G�������%�8�����5^A!�.������v�6p�;xM�k��CE�xU��l�{E��3���s4�	.y�lX*�������-��'������Ql��Ge�����]�^C5��@��#�w�u�����
�E"5&G�z�J%x��h�8�y[�o%���O��l/_���/����W�����n�$����
�zspv�)������-P��f����#�U��Y�W����Y��"�X��
v������?���Hm/�R/�����
�B�5QB��KD%��"����VO����*�w��o!u����~4��>O�o��Rq�]�pP�����UF�vO����Nl�q.�D�u~���_?*A]�����/��rP�������kh���>�[:)��P�5qx�Y��Z��a-��3wt�x�Y���Pb���>����������������E�/�q����X6���JP��%���"��zr*��qC���(��������eG�$y]ZNg�n����t�r�4W�!�2|��ND�����n��L\g���3�?�74�pgoH�
�ffo�:�A1n�\)�gw��o����Y+��
y��NM��v���4	���sGnA������	 �w��>|:�����BU��(B��~�t<jJeA�k�y����k��x>j���8�}����kW��>�'�9{��WK~�����?�����.�z�!z�i�"�j�H�f�/3Yd{�^
f��n�|}��������-_��Q����%�z����!�|.�!�"N��G�Ym���l��P��:����~���HVy�4���zP\._���A�K���h��Y����2
]�J(��R j�l����W�`��f�^�f�S��9���D�J!��!����J@��F���c���J\�3�c@\��~��_3�\��s
hE%�1��;&
�V�������i��=�q
��P�Isaj5�0�<������~�|D�{��"E��W#��~(�]���<�w��+W�����f{��H�U3���X�����0w���_����Y����t��?�B��4m�����zM�������k�Ny���~���i]�_@�6^�7D�$�K���&��@M�|U+��k�R��~Q��A
����"jP������Zp�jU���5^*MDLD���Z���DFB��l�W\�BTj�������0�����X_����1����c�������!a��v�4��j�>��,w���s�f\N��Ff�6��,9C-�r�j����Zn���P#Ubs��zz�a���vv��4�������j-_O���9�����K��7�:��(�K&��h�#��/��F�D�4���^R�����^j�m����V��
endstream
endobj
97 0 obj
   10511
endobj
95 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
99 0 obj
<< /Type /ObjStm
   /Length 100 0 R
   /N 1
   /First 5
   /Filter /FlateDecode
>>
stream
x���P0�����J�
endstream
endobj
100 0 obj
   17
endobj
103 0 obj
<< /Length 104 0 R
   /Filter /FlateDecode
>>
stream
x��]K�-�q���8�g��ck�` @�\ �/�RlXv,p�����Hv���G�@�Z�����b=��!������:8�������&������/�������/oF���J��ee1%��K���?������?��|������>���o~���}>>��w������������7%�v���0�=�|����o�����{��n�)7���7)�N&��)x�<�a����&�G�BI��V��"����w����MA��t���(|�����GPZ����vjLF���A(c���|�Uq.)a����:����r"8����y+�������&��X����1z�C������$N�(��T�����d&)�1�m�VBh��[��q�@�q��+��xb;>�1�����1�����j���	o�W������q�c�U���A��\��An)t_
�E�\I+�����d:��'O���6yi��<�1R�v3��?F���9)�U�����#��Y{!�Z��i�Y�67@R_r�����0�L�cd����
^������c;�����#Y&g�rV���c)�����%]���#���`m�9c��*eFt�R��J`A��H���e���F�[��Lzt��t������LP��1�i�0i���Z6�Q�ZG6I|�JF�?F6����T�����������#9�v�P7cm�)��:-�LK`�:� �t�2���"�N=�E�8c�1I���q�#s7�oo��������;e^$O.����	d��"�4�D �t�Z�@�m�������.�+Od�+u|�A�0���MG�KOu�A�l����#�#~� /O<��?&},�x��zg��L�������J��31XO�� ����9���Jn��������������{o�L����P�/�)(��y�����7dS��l�9�5J6oAzX��/�g#~��n�9��y���������-�:����tX�xx��q�����qM��R��R�z��	G�n)�}!MQ	��M�|hoD1��R"�"#t�&t��0���g?Z���t��e�,�.t/�2�)@�L=��i�����
�N�>YO�-��1�����d��b!�B���T����9![\����b�H����;d_�'�!�0��1�AOdPM;T^t2<G&rr���i�d�o9��j�h)����M/x��n�8aZT��-���Bp��\;��(��m�����u#5�q���j	P6��0<Q�4��q�M��]g�������y��`g������1���uA��D))�1�S�*x\dS��lqm�`����(y�{�d�o�r��������.'����������+rH��v�PG�������=�N-\�(�Tx��cS{aM�@R�A�gQ��P���C��'�O���)���9��c��H�Y{��/����������pp����G��|:2�����|�����v����:���U�kc��tr����A�Z�z���������l��J��x�Q�4>F����-F�$t������I����]&�P����u?W�����4�T���������tBU.#�^�d�W���{1�+U���j�H�����~���;C\�"�I8G�Le����sL�k�l��Z\�r��&T�@�k�w���"�G�X�X��V��y��cp���j9J����(
�)J=��K��\L/�������%I�Vh�����9�ZlI��'�����N4�3�L����0262����Pw������,�MGc���[�Y����{���:�C����rW���(�8WY���\���@�������xe�bx��;yL�'#���;/;B�����;gRF�Fy��p��`z=���)���'�H�i�+x���[�E��
���cy`V����IU����qH6�,�-�v��D�N�v�]�CSbs"mmu�Q����9r��#����]�4���K�#s������x�����[!w_�s�U��8x\"d��
;
���Q��qv�M����%w4B�~;:VK��52@w{\TK���7@�7���5�w��]#7Q(M�7�&6dL4x��j�[K�6v�t�����
���+H�.i�m2H����l�Tq���b�
�_b�.X�#~G�f�UADK�3*p�W<�2��E���1"u,�.���;�xwt�3#5T����L����x�l�I�x6WY4�Z�����3�/F�t��p�(���C�p��L�pV�!�~�#��F��r���mG�����I�L.����*BM�L��!��9a�9Ek�����	�W1j��
�M%�l��1�h$���+6@f���s|��>�w{�\����N2�uL'�������.��X�>��H;�C����5�D���t�.��	�l���x ���#JunL�M't�����H���s-�A�9������c�N�R2Qv�1m�M�WB������g����# Ud���^����6�|��3P�~d�
����[q�P/���E��D;ei��+���;��s���C�*V
U���w�g$�*�<�h�����!#$~s^��G��1^�Jm�x�cD>���9�~w���"�^XMf���g�B#!u4�D��>��}���i�����U!=&���N�%�5N	_HE����8��,���J]��g4_������u�-X0�e�(w4G.<�{'�B/e:��#��b���v���;��$����������=��#�^����17���Y�~��
�S�Dt���P��h��:��5��f�t��w��n��}!Gn�����8b���8�� [\$��Q�b�H����#~���
��	G�
���c,Gw\DTK�s������k��D��#�����^������;�����D�n��Q��j2��l�"I������������3"d��~t���b�+l��%�������M���}t���n:���6	_��38�w���YIR�� ��tF�H�z�������"�"�[�����xW�P?��zDNF��`�%��z��4�hz�$���Y5���Y���s�rtIS���]�IDw'7
%M��r�O5��g��� ��t.���hb���f��8����+��J�&�"��@/�o���3\�����1�*<��Uv=��N{�n�m.|�a���<:sI��<�n&���SG�O���C�{���o��������0c"C�
{��3$��_�[b�^�`���p��j.�&�}r4����S<Un���]t*����&���J��]��(�Urw�]�7{����uBv���"9�
e��e��l��in��B����
�cl�,��7~G��AvV����9���MYk�B������+]!=�u���;����>�[EP���5e���@z������6����x�5�wd��\XY��>:�\~��-0�@�q�r�#�|x{w#s<��BD�x��������c�	�o���������3H�wK��d��e;!�w��Dr��������$��9#����tT��G�w;<�����E �1�G)��yH~�@7b����B92�t�U$$��w�ig�jq�F��A�!/�^�41�}�N��t�9���YI�)�tN&)!�gJ��H	��	������0N��t]r�'�����g��y ���Qg�M�E�7cd��;��
�����1��A�)5h��@��c�G��>��!����?8��w����d���?���_�G'+	�~��J�w&<qF5����`|��3�c�h����A"GQ�{�^����T"<r���B2�f�������!7�w�����ec:�Y��J$��)s�LVV�^�V����O�]E��&~�pbp�r�t�\��9�;��=c|'-�c3K�%�)���} =d�����cldq_(�������8�� ��Q����S(Iw|"�j��b�����`:9CS���eJ/ 'Un|}��d��&n�����b�b�,�~�xNmdL������5r��%%z_�q��cp'������po�������M��PJ���w�O(~��fz�g�]U��!�'c�k��47V���)/�}�j�
��A��������_(��0�N��t��cG�-�F�V�H2�Qf$��!cL�G�����
������^��4�wk��������#�4���7����z����oR��woJ>������m?������y�Sz|x+������^h�|���7oR�(�l����7��N�����g������-P'��8l|��:
v��'[h�B�Y��Z�|SRG��-T{��C��B�.n	s�9����d��SB�Yl��	���lOvPN6c(>�A�O��5�O����j%�U~��X�Y`;���On�%x����
>�	�2�}
����\O�8��n��
&d}���!	c��,O�}F7w���d�gLB�0!�N����$���'�>S����d��J��4���6
��h;b��0�O���6
���-�8��g��FH7c��}�:�$�1�G{Y-�����l^�[��l���<�O. �0vF�'��s�PN����G�^����������DR���G[,|)5�J�=��u���<�c�ZNk�=�cM����h���)�M*�=����q��=�c�2L��=��
"�b.O��|�g�fU{��� ���m��X��hr{��
��'������J�8�����	�&M���Q
��~�����BF��-���&)��VB{t���6c��-�c����]�&1o���R>s���h�5pD�VB{�������v��}�.
m�����A�8����6��~��������gU{��7a�������P�!�m��pNns{t�ET~��G{����i%�G���!?�����7+$��~�K� �KZ����:�Cx|������P�/��}}��M|*g��o�����M�
�����O�E����*�?L������H*�XOe��_�����06X�
n=
6���������7�����{�P�������P���/:��=�p5d����������OI��c�|*��s��l��AXG'�,���st��c���}:k�?�Q�	�n����^��CK"��(����aUO���X�F����T�_,�g�B'�]����p.�����M�WB��u�m��Na�����^����4���a,	��	�=x�L����RB�:��p2���Bjt���<t����K)��jj~-?�a��bnc��1�[�+s����`���	O%�?��i+��s1i�]
�8Vkl"�4��	�����WZ�~���R(,=�kk�m��\��>����t/l4���Z�w+#2����6��2B��mn������,#;1Y��`�"%X'Q5����`���G��Nl���a��$`>�e�u����s�/3�I��\J&\y�z�D��&�&��2J8���F���H�u�]l�WB��vV�K44����Ax����i��U�`����)�7�6I5�mcx�����O����_�G���uz*c��U+�
(T#��2�������#���Y��HD{��@0���z�E����	��b�::Y��_)�j�$�L�P��<�4�}�hm��*�����3�* �}^��m����	`�n��8�R�<�ipG��X�}_����U��
\����L��1E�Y	X8f���
����,��P����n��nu�CibOH}>*eZ#�
����1�.������F���_4�4�ak��������Z�e�e2t���r��r�t��
�6��KB�7QX�������a$|=��+
;\re���!o`XH��
���"�Y?��R�F8K4���U�_���a�\&��'��@�6�I��98��f���S��q2�+��-~mw(S�V������2��<%a%�`L+u����4��1�d���q���K*�������?�E�������v�lj�T��p`k�.R��\�p�.1��0������?q�p"��JAT�<���h������������9��N�V�.��U���Lhb�L��^s!h���n���q�W��W(+�{�x~0�^������+��E��5N�@l���!��LN���}�c��������7e$������l��|���$~��|���7�5{�T>:�P����S���x��O���>���?���_��f��FG�2�O���+n|WJ���+=l����(B�,m|�������n&�������<S���4C#l��[����0��f��I�l���nF)��pT�,�DQK#���G#���A��LW^�j�� $an�,�l�_J��'������4e\�R3U��0����Z
^��a��b���h[,�������W�W��sJ�(B�bNZHM����g�"�&�(�bN�ExB2
k�%|9�SE���y�sRe�e1l�!��&e�s?]��+Z��
���iI����^^�N�u��^}.+��0"cZk������6P�5�-��6���rpN��@x��sW��|#�7j�+�l�(����O�	$8�R;�&�[�s*���2��?R������az���y�_�r���jZ.�W�����7��c{�����O�H�������;Ok���4yZ;
J=�G`�����!��� us����c�105�q�p��
�0xQ;8�N�Z
y�>)��SY~�����o��+I��%\�Vm�q�����������erv��	��12�:�*:�s�����#�$��a9[�����T�7��c�
!Cr-p5���-V�i;L=�q+U"�N���1���%|��+�I�XR7@�Z��U�d��=�S��l����v�%����^���b�iT�9���~h-f����Q���3�8�6�ev(���Y�������4�/b`��}��/�$\{q]��`�YN���J-���+��Zp%�\'��A�y_��i� �3�������tr��-����S"a�(_=�e��y�i ���s�R1�*�px�O�j ����y9��a8Um|�^����^�|��-r�
��>�]��E��W�R�z�K��"���
��:��hXT�\f4L"�l�It�t?"ju��+�����8���A������7By\�*�57MLGk'Js�P��6���X� o�w���������.c��-��s����>w��l�����Ui-h�
]�+L����A�Z�&h�Feo	A�O�:�����'������O������*7��	(r�Tz/�I���
�V������sS��{� Ml���c��zr�bO�����I|�5�	�O��Lg����r��q�t����#]Nh��v�F65�������u>����SU�N��-(	�\!�q2���_M�����L��s����h���Tu;�����U�Vj���p��`-���@�,t�!�#�\TL����=yEU�Ik�U�_�P��Q�(f>9�������rw���OrV���g0	��Z�v��BhHk5������V�X�9v���Uo����m�:�_�����v���-���79�`�s����	j�k0��)	0����c��IK�P�0�R��I}�#�h���1����^�B\�7lC�F(P�I�+i�� �Z~
�C�F��JZ�����df<�\	�t����s��=�N)d�$(��c��*��3f7g��2��7�fZ�/;,�6�x�R-��rV�Y����������S��x����<��[�K����J�KA��������U����]��	����o�H$D�t"��t�HK�������}G�y^n�������s�������"y�����"�j��gJ������%Obs6UK�
kQ�\qBvUs���YtW�����7����#T�7C��t��3e�������1
yR:'��IBu$4�A�!K��@5��9��yD�@�)��'���>�f�����G�����(H�R8/��D�"��i�Q��}�h��r=���XCC�%�n�N����C(�Q2�(�
ql���0������Tt�����
G�	�',���^68�kN@q�����k�N�*7��
����tK�%Q!&3T���q�A�=�,q5�K�����d��z%*		���cI�S[Y���QI�{T�$�V8�
mV�����_�	#^�PG��������p�"���/�a�"�^�vLt���Q���a�i��\���#�T����)uc���VC��bJ]�*��`s�w2(����Q�D��<���V�Z�(�f�S���f�'���>���%�5�~�S~�8p��!q���)�4U}���o
9S]�7e��*�X�����C��������0{�bM�~�h%���S����K!�UV�|����?ViB?���br��%��9RV�r�h
��*�&���p���=D���J3�v���ho�9�V��Tk�^�r���-��tx���Y�'���ht4hJk�'p�� �<M���$�����j0yo��e���2�Y��v�����*����-y&l	�/�0O�-!�!La�r��	3N��G�r��G��,����=�gCE�jm��l/��&T,��I4��m����N���
��n�F"GE�<�k����EA�_d�r��n�M>%h	�J��5Nk#�X��Z��\���\��������4�D|�#���]�r��j���>�t�q�� 6CY��+�1���[�T���d�@�K��p��C�-Qd�
f�[����}�������O�{�K���n!k;\H��.	Q[*���p.*<-�C���%W������jY���#���krM��9u6_P�M�N�n�T.�������K-	��
�*���{t�����)�2�M�$"������5���p���Gv�����%��)Mq�L�A��R[d��v�_D0[b�kw3d�3L�������9���#>�}�C�[���i
��	���%��1��AQJSi�"���a��&�����P�P���}�+lE%������1s�������O����!�����
JG�.z��Cn,\�<&����?��)���YK�3���j��B2]L������J
j3����0*r'i{a-xg�i;�j�=�(�L=�3[�i.�9���E%2���K���1��\Ve���on`"�a%J�lg�k����T�M�`:��x_C�1�{�h�a�Z�W�n����ZK[k��Z=��z��U2�`@+K���.$�+%7���<>���n�
�)Y8���#��_\G`�:e�?���w�)�A��|����S~1�h��F��6��G8���}k�����hT��!����!�x�ad������in�-_����'R�F�C�4"<P�ipy��	;o�sHb�#g4���5�1(�$tV��y%k�T~KE%KH]�x�wWM����G)��v����T���9	�(r}�x4�|[{��o	W-��,j �C��U�������	s�H�\A�NSd��2U�C��L��,��W�����V���f��i��6&��[���2�L�P?����X�o�(3^�F�$�.��pT���pn
)�J��(b������d���ma�I�wT9�P�s����%i5�D����!����`D�8����]�tU�W�-,i��s��DQ)kAL�;��;�y���f����{�f��`a��AbV���-�r����j6�S�sQ��o��7�H�����h��K�������e�4a`�&L�hB;���	�\M���&��e�HK}2]��(�D���^��2aN����xu��R�Upw7�a�@���L5����.>Z���x}�Jr�Z�p�v�=����U�����Qr�2A'C+�:Y�:�u0�x��41������2���S���-5Nv�2dd�l�������J�?�1���Q��.�=�Z��e��j��\����:������m���������1�����7LW��v�2����[x���Ci��R�����qo5��1�� ��������4���s��nw'��m�2(e4Q������f#B����T�k�C���%�R�w-�!@�sA�h��V+��8��y{����/��n�[��\����`1?�<�U�s���
i�^m4�^�����Mqio��1��Rn���L�Nk��E���!�%�������x�+n�M�&"�j��Sx�����T
�?���YA������,���e	CH4�[���6�7$��s�p�OG�!B����T��5,�(��T1�8��o�+��e}��q�!�E9�~�MO��y��bC��������Pp����e��O�����Bf��3��r����C���~�=(���`�E i�0��=�l�ZL�X>����\����F�R�����E�vXn.����r��JO��kZ��v��q�
q����.����&�q@�.�!i���7���p����{�J��qR.N�md
��Fb��,k=�^��?f����
�\���B9M�
PCy�tc�G���|���R�>�sZ�������Sj{(�E�n��=���vU/��C�^�\�,1�?���,
��&r�:J���U�c�s����0���LD����\���#&����k���h |���K`�`P��Z�c8��!��T��w�i�8���*is�F=�G�O��]�{����;��P�����Fu��a�K"3j�9����r��4H2����&�t�M[���s��{8��n�8f
�o,��@��GW����������������V����7#������]I�h�2��$q�"z�M����_b��p �.��X\�5�M������R��Rn��=ic@��������6�*v�������7|��g<)����A����C)a�������=��%�r{�%2�cVW�������1*�V��	��~�`IZGf��A�'/��r��7��?E)�f(��ws��j�8�WU�7����A���N�bk)C�����)"Xr�0"�>�����uc,��6�q��E.�M��I�������E~G���T;m����������8�;W��h�����]������U��=���Q�O�
Z��X���S��!���� |b35�]�w�8]>�y�Z7|��`����f��i�������l���������U]��
�X���w�i����{�]v[��2�D�jh�89��4���l	Yp�d���z��'k�t��?�����9:x.y�=� ��d���V�����BFjK\�9eW�~���_�9�H�jE�,l��kb.I~�_�_���2���2��
�W
�n2Fh|�Q�n��;��\")-��cc�e`	�j]r.�)���^���+L�X��j��Z�
�+�s��<�}�l�)��%�KA5���x���LqB�$�����C)��p������)ii�_�����w�����1�|���+vS��c���sZ��������]��/�QennC�eG�����q3u|��,������8|y���'%7
endstream
endobj
104 0 obj
   11556
endobj
102 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
106 0 obj
<< /Type /ObjStm
   /Length 107 0 R
   /N 1
   /First 6
   /Filter /FlateDecode
>>
stream
x�340U0�����	��
endstream
endobj
107 0 obj
   18
endobj
110 0 obj
<< /Length 111 0 R
   /Filter /FlateDecode
>>
stream
x��]K�%�q��_q6��4�on�c�,^�%���X6���l�Ud7���jm��X�����&�������2���o|���>���?<~��~������fU�9A�D�/��J9��A���v�����_��������������O��x��>������o���w��J�x��M�����/����}���G���S�~��
���x�X�N�h�)kN�?����?��C���v��(t�s���*z'�p[BpZ�`^^BN��y�+�D��
IX�V9��Ae���G�6�B����[�`�QY-,�)�6�)�"G������a��7��	����&r^'G��U�!Y;xc���4�u�j$�`%�\��R2��?���G��S�9�:O�r�0{GN"�vD��%'<r�Hq��g���#g�:R���W����L��8x����yo��YH,�1B#�ziZ�5�L�@����J�/&�3�1�6��wZ���������p��ihf������
G��d��1�����t��U%ri�78%r�]�E��8�*�4
�c@u�sW�+:Ui��
J����
Ku�pr#Gv^'BT�!��H���A� ��k�',w��5~�N�fF����������sz��W����{}����}r�J/����7��nuW�	V�a7�%g�3�(L{Ei�X,��?F��
Jt!'m2�1B8��d�$����&P����R���bT}?�*�{�M��*�gp������
�	>�+
{t$|���<%�k����
>VBpYz����
�'�&�Q0�����:C ��9����9����a.��3�������;1@�O�����T�����F�7�������������/��Sfn����Y�Lb��/�%^gD��3X��]m]G�%\���#t����1	w��<IUzK����u �op���g&&�x�t���)>�V�Y�yFP\#�K�2��!m�`O�Wc�F�������N�pFP�Y6��4~�PE�L��LY�@�}
SvL����0�`�J��F�.���u��.a,�����~�'��
naT���l�������<�7~�B�/�L�0dBt��B�b�l�1�%qs�������HYC�#�����fy����(\���&KS�v';�2�"���|uF�s$�m�W'!���k��D�,�11�W��0&�<HR�X��������|Js"\���?qJ���UH��e�J�O$'��yC�&=�"O��'
�
�"1���O��i�Kp��Z�w
���);-
��s <��{������G'F�F����,g�L`%���Ey	���O1{�c����������v���'y�R~P�2�`����M`.�����`�L ��7��
�2�y���/I[~lrDb����$�
|jE�-2!���/�qC	n�����.������@V#�.:���C��K���tL�����C�-���*�q��p���;)����p�
��x����W��H"�����^���ML@�;����$�L\��^3G��H��7J����V*y�|�d?�1�dt�[�AR`�d���Qv2�q�����\�NL�w���3;��>�34�����R���"��_�;1�Gt���\��<eT���Ex{��sT���~b07T��������
%����}c1��0��$��u>[mJ�;#�!�4K��ij���/����F4�J��2���7V��:
]��f��huT��%�b������I�ws�	�����8#a4n�_~D�r�RU�~v�������|dGl�r��,��PF�yv ��2�?	qY(��'<+v(1���)�~���a(�P�����EkSM�
"�������q���(M%��}tb�q��ii.�����~���}���{KO�~�'?T�	,!!&���nCP
��������c��S@���KUi�Pd���;�GX���!M|�Sl��mv.�$��W
���� f�#����M���o�h���=��6�4�$�
��$v^���Dr!��Ad�����x	?&��5 0�P����7�����-*���uB��g�-a�^0���*=���v��0tQ{^��v���2���|��@x��Pd8;b���p�XL@����5�����\�%����2ib�����kKN?�Q����	��DZ�-��5�eib�]��w&��
�
�Kr_{�"������?�s��<ZB�K�7j7�n�Hs	G���_[�;�Ez:�t��Z�u	C�n.�)^�C	�g$�����'�����r���!������#��GEzA~:D�wAP����&�T��+��KBX��9~����0�k/F�HtW��@"����|���3����
;��A���TI��y!�����&Bw��B{T�f�r��W&�d�����PqC	~-R����#��oA�C-��2=kX��������Pd7?�3T^�"|���:]�S�_#�2RC�������.�/�Hg���/���_?�L,��h��Tw��.Aa�*y�
��7��1f	�u+�>1�'J��B2~$��(Or-6�v��ud6?w>Q";m)~h�D��/�h\�����pE��l���������""sCoewo�D������}~:1�72�O����Nd6Gw���0����DWr>�'���� H�e"*�����9���2I���G��~�?�,�L��o�	w"/O�gk�q
������[c:�9v(��R}xZ����;J��)&i���<(�5���b�"T����E�m�����J`p#V��)�X�/�f+�y�si���rWU���w�Z����qg�J�mC�����!���0�yO�C)�%�k6v��n	w�E��pv� J*�>yB9�!���/�k�{|^~9��#$������+�^g|G�Y��Zc	�NDL����G���I�5+19i�j��G|��D�NY����w.)g�.^~a��X`�
�w����27��9��i2��������d��q_����[C <�!�!�0��O+���8bq�L�%h�.�C�������-������A�#y�*5�p�0�P�81!8..�/��L�W]�J��w3]<;�~����@Jk�Q���Q�s�5"t�P&;�������(���H�J�fG C��b�u�\��0�1�/s��;�J�!_��Nv�U���|~Lf�~��q(#�<_�sQ�t�3W�����+;��*,��z#����@������d������@x��`�#n�!�����:	��w`��j�(*�������B��
�g���d��T���"��]���Q�F��s
���s���~y�ij�G��������3�x$�$���-]��1�3���J����7��aG 
��M'��].���p��:_��������'�p���+�����#Z��N����7F��� 	*2#���x��%_ ��$@��qv.�H�~��Z���v�h>��'�CN�D��D�/i���,������L��������,�J��{�9�"1�l�;8%�T��1��BAH��=�'��	�a�Nc�����e5���6�m���Dx���9:8,���\7b�����p���E����)���u�����o�����/�{h����~���/�����-[T�����>=~|��e\x�������&]����]#������������]��?������O����
7��'[���7���`Gh�9��!J|�%5�@z �O�����[����#.�dGjK������H]�*�8����,����9*�@�'/HtQ���
>ya-1�is��3n���QD�(���UQY�W��1�i�D�1+k�H��H{�Y>���H��H����xOYi'��d;g�*�i���v��T3e}")�����U���G�����x����+._��6L���~����3m}���X��L�m�	LTY�kn���Z�b�i��=�3�M�����uQY7SO�sz� ���=��P�i�#�$��g��{N"U��g�p����=��)������M�����=�KT
�h;��G�y3(���m���[�-�4o���h��=��G��5���E�?��k�2z����?|���>�jc���D[��l��i�6��~�6�`��m����)�'m����ee������	����� ���=����f^s{���V7���isTy��dKy�~����i�(�'���i�9|���=�A���>��ZPy6'��v'��L�h����~�J�I"��=m9���y��=m�J�I"����t���������]��dO	�H	������8eft���Y�<�O��t�y3m��h?�O��I���=miz����G{��M����=m6��I����Q�>��G[���._�h��=-�
q��?�A��0��G-<H~�������JRi�(?��*��Y��I9!����~���~�x|����B��c
��Ka���>���d
�u����+������M�<�QAzw����� +H��)r�2h��|�;:�C'q���d�8~����lU�][T*��:E���q%�6��
��E��t��0�*�ld�O���KL���>�;��3���:�����F��=2Pnn�.�w����MO��b�������U,�u!<�
�Lua��F+m\,���o��r�H�&�'�\C�$eL]|T:�I�����68���w�{�F��]�>�d�,4�gR�m���~9~����%���a�3X!�@��*����B�A7e}@�n�����lJh�1p�������\_TG�)��l��~��~���w�k��]�]�M���������������l����������������p����&MmRgU��I�<]2��Zg��D����co��%�:�S�"P "P�B �G������r�[���G\s�R��V'\����"�'����&�����{���co�T�m�7����T�k_�cXoNgT"�q���F])_�c����[��Mi���;����-1>�u�k_h�+A�u�,����;��/O�=
r�Ok]��v�H���gMs��f9�R����}�����^	�T9V	r,D��~�(1o��uHH�y�3]m[�I(��XC��{���%����Q��@h�qg���Ga{�����2�=�R����w�b��2o)��A:L$��@la)�&�A�W�7�G���2ZyZG!v6����A4Hye��2�)��X~��������n����;������|Z�l��R��l�*�IUM���
��J"�_j��S!���0he���4�G�$1�(�����Q,�kcT��S�~ ^o�����c��D��'���7]��f��U8W�#R��;s��J�(�� ����;sT���1c��W����#�sD.��:9sd�����p��#���J�G.v��KO�C�\�*��	^e���$�tR���
��5�s�H/,�]��W`�x����!z����y'�Aw'�*����c"�P�K!v����vw8)k�62��.��Aj2O!RHF���y�^�'eQ���A�B$�B��0D2i"A8��ptCPQ�c��P���q��n������'D�����i>����u��	�u�g�}Bt�d�$�q��0�/���qN56�Vy�K���"�a���J����|�"�v7�%>m����' �a��I"��`�"����vn:2P���-b�-��������M��:�u��z�����kI �J]���J����F�rV�_�KK$����UN��������j�pL�/bq\���#���s���H�����C{��^i@}u���C�L���q�M��.!�&`��@W[~�M�1��Rt@�(���DM���o�P�������������~���w�>�`���^����.��
�����C��H]���k�V���B!q�dK�v�T�o��Pc���\���M��D6(����oN�&?���Kv����^��s���,`�����s�&U��%g�'j�J���Jqy��q.$�w5��N���p�Si?������#���r�n��fVI�S�z����"k^>�`8���=������@�*��t�!0���� -�P�q	�/���j���EY�������9�H��<_B�<��[��:�@zP�F�u����F�Q�����M�[B��L`�8��<��29HaYn�r�R��@T�Q�.����Rr�U�C�Q�A�
�����4�*�P�E����{���P��4)!����|��T�g5O����y�Y�S��
�/R)���Wq�x6(����	����*�6�����/�w .�A�J����z�x�6Y����L`t���L\��o]'3��	^%XAX�`[���b��/����>����N�&�J�SX����1�Z�Xz����4R��	������&�=�	Z�
3*�����&(�w2�q��?(���V�������L5'��c����lP3$e����J�������������4N��8T7��MI�,MJ�Yb�E����G��qfV|�������|��$�^����+FeR[�=2�Uu��m���m������fv)�C�bn?�V�����������?����
��.:w�����z�@��^7��H����wH
�#$���z��G{JY���'X����?DTbIb�	�c1Q��K���1�2Y�)��:�;j	U��P����T]V�x�K��������o����X�0�?��o�|���O����o������p��ACH�=�/'N\�����	��O�W�
��/L����������a��75����e��=+���*�k���Pr�b�{�b�%���5���v'����{M��Lk"����&k<400�$=V!zx`9�4^����C�&^�q1'�<D6�`��/q�Q=��S?�%B����FK���4R��\�I�����dv�;P���}�gh���v�fN��TN)��RLL�3�Z��2{�4�9c=��!IQk������s�,���,	�J}4�F�s��t���FbF����"������E:�&���9����1��x�L)�`
i[����j��L����Z��+�Q�a����s�z��;l�">�b1FK��HiHK��R,JoD�%�Cb���k"��%��Q���0��03B�'I����b_�Ns]��+�.��s[�KC9�&F�h[w���\92�#j?������s���)�jl!T���:v���b�p�b���s^j���S���t�����q�	4����q�~n�)�]�v��C�m:��H��!�I�����
�)��|�=_��%IX���f]����k���c�9Q�E�}I3��H�SL��"�1�����Xw8�>�)��)���t Ns�>z�P�+�xQ>�Z���������{�jx���73�N]s��KB���wcO�A����7�����5�zT�!�82l�A*�Fb���l����������r�
��<l��7��!��^�����;�z�%ft.�������9�|�-X;�<�#�(�����L�������c=�:W���H��������b�����w�����AB��OQc���V�Q�-�.�E���=��[�:�Y�w	����L��r;��Ml���������S$�����
1�l�Z�x�a/m�,��F��;�&�W��=����oi�d9���k���e3����6����`�3P�R6�q5�g�$\'�	��x ��U/�������L���,��5�m ��Q]�m!��4Q�K������E�M��(���K�?��d�C;�[:�^4�Ge�t����)�J��u!������}�Z/�n�zv]P�A�vP��A����I���+J"I���H|lC^���yg�La�<����R6S���a���}�:<S��yE��`DE%Y�w~��"Ot�d$NCr�������C=2�`rg���.w��.�q�_�.���,�sB/��������\RO��coa���1�![��s�-sx�G��&7f�O)?��vh�>�}k)���imZ����������<��	��<{�9�����.���tXJc>jp��pmw_����};v��c=�����@����2���V���nH����,�9��7M���b�7�e����� ����#*nZ���Vy'y�u�K�O�u�Ts"�V����
��b:�"eb�����>FM���(���z.��!�E�z.Qj���r���#>��<�To�=��J�����C�1Gk���k�k#�P�4������w�\��n�_��������ng���4�H��b�D.�(���V�s����"W5_7S�;j��N�#cud��;j"c;M<���w�`��vO��V����#������>.�)��xz�x��(����R(@Q*���ze�*yT	�#���N����
o{>���k���uJgwj`��tT��RZ\��X����	I��4�G����m��xD��R��`y>G�[>��v�Q
��IP,Y
�`�'lb��������j�j9��k�[�s�~�g��.�-�B	x��j-�%�P������I������8�����[������������'�.�rw���F����y�^���WFy��W��^��������I����^�L�K���]
q�B�^�<�}�k"�j�/�K��v:��R)^����������En����|Y��r���,p]�>���R\o�`R.)�����K����8��:~)��x3���pl����r�<g)��M;��/�Y�-���f��
S<�j�]���*������\rU_')�/`�j#�{���'���P���G����v�������Yy��j�����T��J���6��R��O�Y�V)�M���d�b����|����%����d'�Pf�����L_����?�=�)�A�W�"zd���uJ=��Nf��Au�/c�v�X��I(!�.����f2�F����S�;��_�"��?z��.{���M�����^���m��*�G�"�jK���1S�6����I��471S�<�� ���������&���)�����
C?�:�W0Sn������p���b2����� sR�z���\��I7��T���V0W�R��+\���k���8��Z��d�4�K�[��l'�^"�
�5"Oms�|�Wd����r�cVrz:�!_3�e1@9O�������Oi�.�����Oe_���=�B�n��8[��9S��|n�����>/�3a�/�=�L��O�mC��t ���VL�Q�,G1�{�(�1c�5W���Tr>n���I��o'Y�F���>���N��P�����W��^G�/������=[�Q_{\�J�v�h�#9�Qc�^Y�2}O�N��e��c\�;O���F�ie5�����+^�\��i�����P�+������6K�:R��`����}�m*&S/]�S�������1IX>��|��O*��rh(hrwz��������	�>��+U��^7�2����	������q�#����E�����03z}���N�g�F�q���+j��(��y�E�D�N�v�n���	�>90�����S��-O?p �+oz<ds��7�����S�bn���u�_<���0^z��/7jZ���r�����0��2'���Fb_f�d;���~9�oy�q�������3<O?�q�
%��[2TU�&P���1��GP���T��'b^�
���������E��E�n��I��V�7�_2��.�w�1��d��p"^��{�2[�/�O���N��_6KF�=S�������1����`�G�e���.N�-Y)�Kr�#���;�B�7G�W�[���8��X�j��-�<��&q�L�m�-LP5y#&�Iv�����Dqv$�fP<L�l�i�L��J���T�����au V��v��a���N�S�������i��V�����9�W���t�u_dsL%���qu�����{�,:*AqN�yC�kFb38��S��F]�@b�6^}���l�_���4S�0f:n���1���g7�{��}�I>�cLk)�n`'.���,�y�	��RW[-<��|o6$��|%��V����z��������0q?�M��S��V��s��[�G8��$b���$/����k}W	��./�\�=��O��T���[S��R5*��S��j��i�R��O;tb����;���"^���P�@���28�ZL�Z�������l�����m ����R��XP+UJ�J7�;��M(�P�����k�ru<���2O�$ZJ�����[{n���@�Y:.�~)��;[��2�:�cv���RV�_��Si_�I��,y��"O���P��V�v�����i��8&��Nk��[����s�T�w����.<��5�!��C�~�����\r=�CC5����E�L�.�\r[����b�|v<����$��6-^�7|�Zn7)
endstream
endobj
111 0 obj
   10540
endobj
109 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
113 0 obj
<< /Type /ObjStm
   /Length 114 0 R
   /N 1
   /First 6
   /Filter /FlateDecode
>>
stream
x�344R0�����	��
endstream
endobj
114 0 obj
   18
endobj
117 0 obj
<< /Length 118 0 R
   /Filter /FlateDecode
>>
stream
x��]K�%����_q6�s�Z���`���L�,
/�~����
����^R�"�y����S*})e�
�+�C>��C=��%'tp.��7?���M�����W_�����7#BJQ�
��YEL���Ri���O?�}������������7�����������y7���������O�{SBj��P�	����o������y�����w�T��o>��G������yx+|�m��+����{|�ooR$e��)��^��C'�b�'%�U,<yi�����^9���0Oo��^�=�����$J���$�l��N�<?F����������;���D��I���=���R$S�?G���N�N}�'����Nu�"Dkd��v��G�6��S�=�������N�`��a��NO�C'#���������?�9&��_�������.�}X$�^��_�����J���H�������	�T��1j�&���qa����h��1�-!��F�F�2��!L�"y�
������� �b�y�#��������i?F�����������opGei���c�C�>\��I�����dDT������>'ET��2�	������{�7P�R������
^���|�A�N���.�#q�>4�tD���V)���t�ad��J��(2��
��H�x�l��=����b;u���������D?�l�m��!:��4�E�9�Y �Fs�����>����l��2Tc;��4Tg�����:�v4�������I�9@)�4ls�����8�
�7Ux8q�����*�����R��j�|Sb0������Te�v��%���Iwd&���O�TKHpc��|�8�G��[lB$0�II�X���������qG�����,��C���F�N����_k�<�7����q��@��a�Gf�]�+d)b�6��8M6E5�pd��������`&�8>�[D���Y+��tZ�:BN�pN�~�����=����;�d��Lo?&�!�
�#(g���4Z�x]{>R��e�H{�#3��������y���NQ�B^�L"���{2��@^MAn�������&m�g��,�9"rd��"90�ja��P�������,"�'�6���<l-�:x�B��L�;"0a��rdB���	�x�a>��+r�}FE���>t%�G�(����P2-wD3�M��n��px����/����y�t��<_��UwG������B+����I��06�X s�d�'fQ]y�y����X������;w���������+�3�N���^����9�yg�f�������Vx�cWBx��?r���\�� �h�Q� �"���u�!p��_1��_�1t]{�74e�`P�Y��>AG2g3�Q�]�-����3��@�9W��Oo����/Q	���&&+�����W$�;�3�%���p\*�9�����
����e
�=�Q��F�����
�pWh���L�g�X_�J��rdf�����+x�|����H@Ns�_�Wn�~6@�����S��)K����!��`QK����I�N�d���������B�ywD`�<n\������u	(	Y�����##w��f����O�{o��K�wB���w����|q(`��wD���ub3�?���GB@}��A�������|a�x������8i�yu���	���	�������l����)�B���4�J�4�G.R=N������.���������PRX�����LW~S������s���#�`G�m����=�X�'���xi
E�OWs~�+���H��^Y���5~s^q�c����Bi"$��#�H<�������?v`�RJ�{����1������#�F�9��f����4YY���dF��Zafd���;!�4�h�9�����E>R@��c�d���O����SL�=��X��H��w����.�{eg1un(&�
5v][/���|�I�ck�R��[�$�^�D��h|p:���[��41��o�������#}�_ 3���_������	�����`<5�������1:^�VW��p
�����WM{����[����iCOB��e�EId��B�.P=��d )�cAS�a@=�Rm�A3��;e\��!����$	d��gU���r��IK�d3@�#�����9,������F�����
���@��09����e�����a2�1p�+�����s�";iNcF�A��'�.���z:��r�P��;����'b��#YK�)[5����c��]��������G>��bC����s7�O�����:A�7n1v�+��L:u��*r#���\�<�QmH��We�b�6��E����V��?:�@,����53�C
��F�*p��J�.��)80�y>w��#��yDu&vR��!#DbJ�f����<��;!�
|eg�/����#F�������xr�Z�J%P�BUq3ko��M:/�zCg*<*�d�k��������2�Ax�-#G������B�NADK��]��8���vew��Mx�H�Y�F$�y'�N��d����s��O�'
��O�����^�kg���d����lHI;Z��928��� �v��W����pL���*#������j����a;"��5���'��������"#�9�R����w�X77g=�(��:��^�J`���M<dz{�11��B~`o��L���\�����!�����gZ'+'�������L����-
���F������,��?\X����	�����p	7F�hIk5m[����X�����/%m^�N�{u``�����XX+
�������"HZ��J	d����4�s�y/�\e����|� O�b��'�fd�Y���t<
�ZH�h�E�r��'��B��������^$2����@A~2��P��#{k���6]d����On,F�8/�!L����p��������Wv�����3e}��w���w�A��6�V_�u�;l%��l���p����{3,s������!	S�>��/�Lt"����{4�x�'�^��ig������#"��n�qdaNW`]�7I�"���@6�����������/����pz��8:*������R�LI�e��p�K7E�_�/Lf�mI"��8U"h�4������Ah�C��U������eQ
���=����q��52i��9@���l��L���	x��\96�)_��D't�Q�9F���)��'~�s��jH��/L
7t%�,��\i���A�4�H�����0S;z���u��=��c�u���X�����U���7����=�����Bq`�8
������)��n�� X�B'^��?�1 ����x�N�?��hC���x3��|���c<M6 ���V�����Y\W�L�i���y��#up�^���L���a3�ie�#bs����cp�9��v;�C�����a�@2>���X
q���f��<��
K��!O)��Ng�;r��y-����=���TF%0n/��n?j?F.�� �V%�#�������c�<}o�)+�o���q'���V({?��/��d�p��
����h;[9���,��B���8����R�1k[���D_-2����W����<Oe���d~d�2�5FD�F���e��Ck��%��v`��N������C�\O�������������@����k`#���ih 0��Ax\�Q<��1�LcH1{<}K
x7�oE��#�\���N}�C�6���I��^c�}Z��@�m�2K<0d��1�n��w�����|.�ls.UOLR���CQ7s�6
�m<\��;�;�8<W}�s��/�]8�:3G����s�����[^�y��D�C%���p���L��.��.�4o���+?7l����V�R�Ff��[������f����G`7�&�'�4r��������x �)��#"���r��6V���rw>>�Y�Ax��T;���udW�?:?��W�k���������0C��J�� �Z�����h���4~�/�+�]�96�8wfRr��"'Ui��;s8�!�������ht�{\�-\�n'��u�����t��)�L&�0�U��HT��F����6{qtd�7R�	x��p�����f
7������:��lD��1�c���y3
:��)/�%>fIX�B�7�gx86c0{�y���:z�o���}sw��V��uL�B9�4|����G�m��9G�7w������!�#�����&i���o���7�����?<���o��|���oU�~�?����y�Sz|x+�����i/����<����<,������3�R���a~����������$��
-[�3��1B{�b����l
;���$#Z�P��ho�B�jdKo��6�|���-��ZvP���fq�B�q-�*_�� ��tk������^_|
���K�����eG��P�5�*3>�4�|5�O�B�-������`�
����I�'dm���t���P1����3��4fk�#�~F��-Ri#�S#�7]������i�5Z?c[�k���[�d�����G�G&7A['7���[a�����A�sa���X/��{w���Q	cgB�����-���M{lr"�y��i�����f�M�Nxs���-w�����z�~\����ro�c�ZN��7]���UN���_�D�lm��k�q�6�i��I7����Xg�V����6��k���Hz�q�i{��"����
�1��M�7����]��.`�H��-[d)W����Z�5q�u3�6��UZ89�mo��
�x3ck��u��Flk���1ak�~\�D�����Z/���mM{�Se�#�5]�z��L����z%d��Eo��9%��i�����Q
��q[��&�[���$��3�[�V������������`���i��Q�QN���.`�����]�m���(O�fhk���/��*�6m������{�~���fhi���e��mM{�(����5�gl��~~����F��}���Xg�	����=�\��[���z#d���5�i��Hn~���7hgy��.`��~~���_WQ7�����s���}[�����Gv�j;�nk��n�r%7M�7]�zg�����l�L���M�R ?�����7��_���a��l�r>I�>
�����������������TQ����}�$R|������ �W:G@=>}�����.�&�{)��aUTO����(rsP����?r!���VD6�\�����STO-��u}��l��)����d�O%���>j���+ �_�>V�>&��{����
J'kbz�T�jz����������?�}(k�Q!%��P��[��>P��h��Q,+�1$�E-�?�Q��d�Q��=�a���Vf���:��"������"�LO���=� d*�Lqm��Q��>����r�
������v�(2����P?T�q�\Pg����:�D�Q;�� �6��93�v���&���($��B��*�Ih3��,��p&�elr�=��z��p�N�{�(��������S��]ia�+����#�����k��8���F	��w����N��[����+x�����[���nw���P���#�_�����������w�������B�.f�T7fV��zcF!
�,%t��Z�DlL����>����a�1����T^��pMm�l$'�7��6�(�jc��� X�,X�I�4.���/�p:e{��A*���IY�#�� ��&�!V��VV�|$Y�GR^�������#�<��g�?�H�g�#K�<L�"������Z�%>�Q�%��3
��,�b#���I@�R�����"���U��Y#��^H@�����Q���')� �6B6x�B��f;��Rv�D�����9%���@r?���+k<�u�mp=������]������O��oW'��Q��PM(���������U(��������]k��(t�h�O�%��(���Ea��z
��9��^�%G!N��k����$����1.�_�O|�����s�x���W���$RU�3�������.���1�U(E0,�����d�R��x�-�w��5)�m��:�����../��}c�B�d?I)I��,cm_�'b�v�����
���{\�N8W�a��9cT�Eh�g^Z	�����[m&w�T.%���6rX��\�Z�U��V��V�C�$u���)��Iy�&��w�O�.G
��jG�&��>o������)'~ x ��|�v���1��$�2 ��$kwr?=�Uu�
�2�c$�����lds(���C��|M3pfZI����ve��(�h�������E�������R��nW>NpK]�Pe�%��QD3�	�i��n�~>�2�K���lg;��%qKt����������H����<��r6���`��:�~���szK:�A?���������";m�5�U��-�%"��u>��V��*����X��6������ &a1�PA�J$��z*�k�xQ���[�0ky�F��7�*���E;�G}�JR]�s4&a�Bh�<�4^������1f�E0�8��Y��E�(�P�~����v�2��Yh�1���)J�t����2=�*��VT$�����l�<M\�c�@8DG�"%[@aM�8��U�fK�3[6�>��P�S�W&���>��my��u]o��b�S	
�i��KS��������S%�'3��� ��I��"�p�n����!I�&WE�1A8��i��W�d��j]!}[�$��H���q�X��+@�������p�<ttuO�a$�B���.��!�����
�D��9���\6���J�Y�!������YG�������:�����r#��^��7��~S\�Z����gJ�t�3���h��h������
�1���:�a��y�n�M�	j���k��y�%��&��������6�&�{m������QXo�����p����j���M��=U�d�d�:�a������.Oo;�<`��_;�jf���	�n}�����A������%z#4zlW�m�����/������O�?���%��^��w�6���Fec$!�L�p���^��gj),q�Iw9�UQ�6QZ5/}T�1��
�,t������Vx��>i[�\���[_�q���P(b��n����������2X��$�����^������-�Jr�_�\��$�p�tq$����m&�_�������[>�
�������:��e;+�yx����3�ZI[�a�UTFN�n�pq'��S8��S\X���D���V���|z�EO6x���� qH��q6>��O�M3m���
������N���AX���io�I"����l��2�,P:p'BbX���
�50T����-(�q��<��	QXR��.Cr�&�����YU�W�%��p��dH�0�D���	�c���3E���k,��m49���Z�)�����o���Hs?HM�<��kb��W�Ls�L�`\B3����Ye
�#������Y='NKI])8��$]��:�� ���Z�$���o��O]=&�J��Vr�RHt�k-���#�p��A�
'�.t��5�o��:\)��;XY=n�(}���������N���B�P�`$�K��!���\)��7)F��9�����f��p�Lz6�]��.�yw�/�I-!�����	t���(�(��J�%}��
q�;����(8��(-l������Q'�+5^����K�BEc��K&�u:�&R_������Q��Sq�Z$���3��l��CR]N!�p�����ykhJ�����a����z'JV�W��|���[��`v�!F����6�]�B��N��x�,�����}���Z[��m
�YN�d���Z��m�b��;)7\�4q��s;�����������+5efU5$�`��m����99�	���;�I_����������
-k���:�iU�����6�A~0��f�����	��G����L�fZ�FY�zp2%� �i��.i`��j�_���9��t�g�������	��wap<N�b�Y�KR���z�7���fY"�0����Dmi������SO����p��i�o�����R~=���"�KY%�������������=s�!���`P���~���f��:?.�!&�/��6-I*��W��X���J�zP{����j{sx]	��J�$������[?�b<�$�xi�S/�h�cN04�0w�@�D��X�$����eRT%�
�N�{�`9
I{I�����0R���B�n�?y{���1W]:hi=������{������sHCvR]��;��	{]P-��hY.�1"����+�&U�+��DW���6���N6�.�Ox�f�~�sfU���I	g���7�C�BJ�t�;ELQS�u�E���=���Ry�(v����@�j�Nl��H���yU�]sa����R�|j�"��W���v���hsBp^�RW����L)�4y��������E�p�+��*���)n���l2��������0vI������`1
5��\�.	��|���t��
���)���;"�?n-�g����}5���B��"�����thi�%b/�GPsT]���������������|XrC�{�����X��Xx�"����qb��W)+,F>R	��+��TW8^�����.�}�%m�q�i;D���rC<����:��C5�W�i0����yR��"�:f'.���z~R�ht���U�F<9���Y$��}�:��k!26B�X��Tfd���R���u6*�����LD1�D��)6�Fe	��2*�B& ,��b
[��$,D���	��l
v����b"�5�����}Z+�KL���R1����)_O�ByK��I�������9?
MP>��T��_S~X�n�N�����_����ygy�B'��5ld����&����k�i�dOnf=[���P���e}���][f���kk�D�<�E���2�����-����F�<xY�V����`;��k�����m��-��y���������-����_B�=[�7�$�p����w��G%��k��t��V����5vj�m���L�*�T���7]��C�[_0 ��??����E�[�WkP���p�Z~Q��+����/kj���Z��
^��C_$����F�B%*�;:��
qpp����5����b��T�|������:�(�^�o�D�i�b�%�| ��a��^ ���Ny�I�|\U���>�B�5\���^�T�X�����YQ���H
w�W��"������Uo�����r.:�P8BF��r��+U����0T��10O���.�t:_��l��*tKQ����,@N��@�EN<Di���U�$�
��v�Q{�<�5��e"&�D�t����G_�>��U�J�$(�C�/����X�L���G��[���M����8��.�T�%�A�|�6�-�	.?�%5<��T���s��m&��������,�w�!QD+�
+��|`�P��)*���:E�4kw��7��-��X_qp�M��qbM������C��jv-&@�(��c�pz�p�0Z�4��h���K#�a����n]%�~+Ov����}$�)P�#�!A~���d��'��+����\��/
���*N��
��n���{�O��M�n�{�5�dj�����Lq���-���wq���qm%*]S�r�7�V� S��&c%������U�z�,QC^'���6��^*�i�+
|����N��q_~o<^����V� <^Z��B��	�!������%�H�����#L�)��u;!i��u��0�h�b���C�2]��IY�@���@�U8����&x2�eD�Ah�9Pc
*��CIR�rs�| Gfu>�~1�I
�>�XIWD���P]����cI�#��3-rL�;��a�w�T���g4��H)�2�g����U�]��
��]�`��/����/���0���Y3(~������$m/d	-����N�Z1�$��%mj��q������&B>��I��n~�M5Q!8�#�z�ES14#/l[����������p��o��@x��r�MuP����������`�.|�!R?�z��Lp����h��#@��5�:������iJ
�b��S��������/�k�.=��M#�9�����6p����t��{���R���u��*If],w���pz:2���F�o^���B���s�$�x���|�����q;(�����e7]5d��q,�C�V�k�@���X��Q�O�[J"�����Nb�B,q:5b��H���h�����xzc�j���B����p&&^8�*V��7KN��)������d��t���O�~�������?�s	V~;�c�<(Y�Gk�3eM{yB�Jl��yt�(��F!?d�*5jZs���pY��!�p�y�B���
�h��
�os���_�Q�d
�����+.��+�f���4&���� 1,�=Z�����F�6�\��LsDr1���v�K����S��
/|� ��L��c����Mp(�����Z�����p�2�Z��X�?A�EY	EWe�_�ej��6���2����c'�����Y�JY��43z��Et�0a�r��h[�?{�QUIgW!XqM������N�g0�\�n����dNBwY#G��1'8��"{���=�v���������R����X��Zv��9� ��C��9��������U�B��rq)�V��EvP^�^"�N?{6,V`�C�u��N�	Q����.�j�`��2
^S��wk1�6`2�`���n[D]���F��w�FK�+a	�xD��N�� a���*��j=����w�0���.X�j���B;*,�T�vK{�p�5_X��mKS��HPFE��G���P�V���8�����9k#K�2���!���C�;^�.��Sk�T]\7[AE��x�,�$x��ju����� �)PJ��tb+?Z��QaY��(�\��kp/u���[A���$u��RR'����P�@�x7d�+E(JZ�B����'"Tp�'\2J
�%s��J zT���(P�Q�GD�mD��D5�[Qg�s:l��Z��KcC^�w����"�j��1n3U��%T7H�{o��A���C��_��H���E*�P8��~T����T�)����Vnn:��.UA��c��G
���3~D���h��	dy@�	-�h�V� ]���0d���(,��k��JO�����=�+������H��_��k�2\/b���E�`-�D]�0�_-.��� 4���A[�p�zMR�
�2���a�m������{SF>l2m���������	?����������o���eG�s��M9�������Q����?�����(O��c �����gB�=Aa�O�
�����9�0�k������yO�����s���+��}#���d�qu����p�h��&�x��]��0m�G�&H��'�i�~�_��"��Jt5

�6��������l���S��:z��Vp�%m&�	�E��>����,���@������R��4|*3�Q�������H�%��Qn�({�Tf��KGD���
����i�k|X�~���@��!"�r�J,���lSs �����M���(we+^\]	#X�z�s8�8��d/���V`*�� �*�.b�++�X�*0��J0#�)PG������}��H6VK#�c��g� �
oc:,R�_oKy��Ac������>m��/�Tpkf����X����C^OB��^c�k
,mS\}�vV��o�n��v��.�!=i��P��8 ���>����gr�&7"�|�5Uf��oC��L����iL"���3��b�������v`f%���C�+�
endstream
endobj
118 0 obj
   12007
endobj
116 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
120 0 obj
<< /Type /ObjStm
   /Length 121 0 R
   /N 1
   /First 6
   /Filter /FlateDecode
>>
stream
x�34�T0�����	��
endstream
endobj
121 0 obj
   18
endobj
124 0 obj
<< /Length 125 0 R
   /Filter /FlateDecode
>>
stream
x��}K�57r��~��4P5@�|sk����3�N�� �dV�����_A2�f���BW)�"3����/����|�h���Fx��������~���������z��ooZ����22�������RK�^�~y��O��S��������������y}���?>��������������B*��e��������������������*7��oo�J�����:/@��3�y�RFh�2"8���/�������Q�Q�G�E���=8��[C�tF
�� ���	��G������
V\�f\|�T�7{	x�����2n�Da��8#.%m�1rC���7�7T����o��9r�t�������n�/��|��F�7BOQKSD�Rc�b����F ���@�#ce0��K�rG��G����-��s�**?8����@�����BG�����[�C�h����#C�(�QF��Lt��N?T�q����!W���E#%����rbp���l����Vu$N���2���.�0�rW��uQ�M�#� |�~���
�q�MxeaZ$|��j��
�������|#
vh9��������Ep��I�s���\���.���a�[W6H?q)O_�Q4������E�\���X���i\Q8)Bp�e��>o�E�����6�ct?]�w�w����#�hh�_F�P��C�]WH-�r�������d��YsTqxg4@�6�` 
���0��
��;T��jt����55�U#� qz1�B�'����r����xp��s���>:+���?�r��P������y�Fp�
'�7�5��>���C�w���������������':Y�nv d3�v�3��J�)/i���5��1v�1��)�1��;��<�`z�I�G�Dt4���x2+�^�#�"9d�����[���	xZ]|'8p���x7�;d���������8��� Z6?s4��8�!vxt���48�t�zS����������V�����Gx��0��1����4�hx�V�`���J����zG��iuQ=u���:~A�1�K��n���Z�vO�0H<�������7����F��v���qd����!���U�"c�����I��rh��w0/%����#*�F��GG�I
��d�.��������y���3�#$���C���ELV������/������gZ��#�V	�q?SoDg9<F)j�'9^�L&�_%�t��6���h<>�	��x��v�����e����dy��E5�S�J&c����7���f���oB�6����������c$���0��]i$��-���k42�+��}R&n�6�����L�1��u��q��&�O+p
�O��� 7;��`��kLS��?�����v:�Q4���E�a��vI���?iC~��9�AX��I�\0M������lzH�4�8�
��3Y�l���/Nf�N$�t�k$�2AD��pp�9��<�O����?-���o���{�D6�����v�
�b��d�@d����
8OK��
#>8��3���o3�3���j.d��o����b�tm��;��������1M}Ry���Tr�#�^�"��U���Gr���3O����#����P�|2�	L�����n�w���7�1�����A��*���<2H��K�5"���v�o��M��� �w~�K����F����\%�7#�����5��x��������������'��H5�k+�i���"=����!��������U^6d�<��C'�()�RIf�V�����5�����t:&z��Yb��i�;��y��o-N�c�������|9���
��J��Cx
�����Z)��|��v��?��8H�H������wB{�JIjW���'[b"������m�pp.��(��dA�4�O<1=��~|�l����O��#�A�h��
�'��H�I���;���	�q0>o�\�����?xt�1��UJ����	"[ d���)��H���m\<�Y�'�N�g�0_?tB��"��7��6G�<��p'��E�#eQ�*���s����9RG����4~)+`G��U��cW�����@n�LY�APAm&���2�1d��G�%�F�)~���\
-�g$8i0u%���"��,���qW��j)E�&�n;�%4r���v���kd�=��#�f��� ��&���������4^�Z�1��^	�G�fMa-d�y9n#ol�V�l�q�jlHa�����Gz�Ji	��������~v���B���5"I�L����	1�8�`4���i�r��2��;Q^bt2`����2��|X-������}t�������@������b��/�F�-��4�]<�Zc���c�����A�D�!�aN��~q���f�����BBj�J�}�� �(�O_	 ��7�k���-��P�cfSC�F�ImDMr��J7?����X}t����Q4r@:Tt�����<��w�^c�1����d�/�Y��5�>F�;Gp���=8�� ;b:�|��' Yy3���r=_��P�75q���"�"9d����#��hTqx& ���x��c<
�U�������$&4�����oQ����,�]���&��9�s���@W]��n������b��}�q��c"�]�s�1�����8������9"�3.�����#�D0�z�Ax���-���Y��7�wF�c�p�<�O��/�iu�Kbk�J��]����Ix���L��e�4ek���m�D�=�&�<�|'��`A�@g���i���1Pd�q��A���	�k������x"��	m�X'@?q��HH9$�=~�yd��Z�����7�r���	����x�����kA��B&���i<�x*�x�b�H$�1K~>�F$�/�������#cA
��p#
�������wT(3�K���t��I8d9�<-���t��P�I������G�iK���2�'��>2�P6��x���N��(�}��B�H�|a���ot��a���dX���t��6#c���
��������O��������;��
��Jb���Z�$��*N�
��"}<�1s4P
��p��:)�FNe;��s��z8�>�S0���=6:'���C'��f 
v�#[ 'd�eQqM�B��c?	���N�t�
�Y�'�NEh�`�|!s��QD���^���(_�A��������sOH���D�S!ur������F�)�?�s���i'�y�7p�Lgeh�/W��J]k���8P��b��MW	5��H��������N����;���3:d&�>�w�up��� �&�F���]����$v�����'��Q'n�Lo�+����g�0��Rl���['d98s
���M_���+u��5�DDk��;f��xZ��Iil� ��IA2�t2��F����uHo�\l����WO�g��1�i!��Tr�7�O�3x����yi��?F�H�Ms���zg2�����<)O^��:s>�����1:u��M4~� �����9<Sd�yd��q���?-A�/��Dc�+�Yh���4:I�t�mq��N��NG����zc�����O��*=tB��h�6<Q��'<v�O�������Q����zs�
���<tB^�d��7���u<p�k�1�+gd�{_���Uc��j�}�����?q����!e^���~'��O�a�A�h�B��S���)�����h$��Ki���>7�O
����^Hx����D����`��v��aIc�������"!d����R���OlZ	��>�{�d��Nlr��j��pAi���A����q�C�);���auL�M��WV��cX+���/�s1 Y�I�����~P���f�6��b`������d�����AV@���L���!m#������2�O���(;�Q6��o|	�����4|���Ho���u���x��d�V����/��R����������o���%�|�����^��*�����~y�N�_��o���W{`��_�����}��[>]�v��~[�F����=���
i��t`{p�(���O�P���9B��#�����Y��J�|��#���\fz��'	��	� `��=8�&����T'Z��ONP�b���C��f�n>{�`������03:)����<�wpY\��$��P�5�TI&��7�x����|�M��QH�Gd}rDF-�T#�>9"�gxL��Ggl�&e�	[q�ZZ��L����M�������);a��3�(a�-O�H���Gh}t�z�s��Gglp"U8������ �������tO��'r{t��zs{���$��������_ETlyt��(��VB{t�����4����(���\����
l���Q;�F!��-O��:-����>��
�;c��3�k������
ls���QO��2�����s�!s�������+O��|IA�F���z������JXi&l}t���<�vz��G�~��6����n`}FL����� ����>��u����GG��
7������q���}��3�B&k��Gg~�K�h������!�����q�j�����\���V8�flyt�),�c����
V���&lyt�7���������*�������_#�rs{t�s34?9#�3�>:J9U��p��-���*#������(d����y�:��3�>:��TQ�����y�F�����n`����o��X������GgY� �3�<:��
H�����
����[������h����I�~��:7�o}t�A	;�����3R�J��}��������[�e�y��G�~S� ��}��X'��q����S���i����_p�j����QV9�9{�����1+Q����c-�������9���Wf��?�����N{TH������R���(�{� �����/o��S~�������;D>>}p����������^H*�K^�~|��]
�?>u����$�����J�PA8i��pJh��i��	�Q��*y@�S�:HX%kd�!D�nun=���8�O�	h@�o#��Cj�k��S��A���k�eo�N������o��AX!�Q����X�bt��3�����u"���P�#o������Zy`�rL�������M���T�[[��\@�yi��Z[�T
�u)��6�H�y|��(_/��9�:��||���{:\��&�\N__?F�S�����N3L���q^]�����v?/���������O_�B)C_\��l7I�
pS��FV9e�[N��'Ts�m!�@��m4�:���6�3�	*)0��:�m������OL�����}F���6v\
���f��������pgF;���>�hZc$�v��k{��1�P5'��N���|�\�w4��#_���]����v���#��������%C��}'�"�V�N���N��A@�k)���n�[����]p�4������Ei��������SZ*��(Ls
�G&��f7I�~[?,��#N�
Tr�r���pY^���Ve��I"��kV��ML����0����}���3��>i���s�1Xe�|�����r#�m��
�������S����VD�a�5�Z�7��D|��j?�}�� k�����Y�od��*1/ T����I�,��,��$��3:}�h�2��
8��aAx�-@�����j��3:����x�Sn3����]Q ���W�������+`�����bU�A�}�a�� �lz�q\��������=LJ����i���t
C���e�&��!������b�*����Y%�����K�{�a�6��b6���AVh^�8N��p�M�>�h],����s��uGw���
���xx����
o����0m��P��������;E��piN�)���b����<z�';z2�G�������Sv�Z�hrK?GU���ISX)<(�����������������	��w���0� �t����=^x�[�i~�#"N�ro���:)E3@�*l/�r���`��_�A(�g��}��$#]�h�c��V������W�Q}|z
*�A�	�<��V��*i���U��C�bH39
Y6Jx�1�����2^�
>��O�UL�^
�8�H2{�U��Ci:<�[�����uS����_V���x��VD-���9�Q^<d�Yk��!��t��Z���
Y��=�e������MO������Z���J^��1����D3�	�>l�~29�z=�������<%{�KP��>6�.�[��>6���U���E�R	���"����B�
�bJ������a+y���"y���Q�z��4�UJ�0`"�m�z��2�m	�U��������
#�y]��t^[V�r�Q��lHS.�T.~��O�����]���UH�!d���T��K�`���V��Y��4G^��e�CBxW�h���������
6�mI
~�k�����2v=,���������S���fT�lJ���mjB�������l���V
��c���Z�X?��s0�:o�r����(���I�'q��1���G���� ����b����s^���]��f�h�H]����_4��z��<*KM�n��xV �+b0���7��)�X�����jm�'��'���+�!����G^�jp��:��QT��Z&=p�*���
_� k�q5DZR<�����`Z(t39s1"��������V�����1~U#�*6'�.��U�����o��x�C:�8�}
Qv����xcn�
NC�����6�������F��������������c��.�@D����#��l�d�vUMK9Z�r�L.Rj�2o���u
�B~�9'#�+��"2���F0����Y����F�$#�E�e�B�������R�����E��V1���5��(��}�������>*���4�]S��ijfeP��(�9��4R\
���������������m������$���j��Y�To-��*���+���~��*���h�����&� H��/����������"QR����/����5+y��FU���F��J����N�.�J�
����h"��t�3����M����1�����b��J�H��{���=�m�T�|%?���k>V����4����I_�B�1����(��,cC-���dE�CRC��h?W#\&y���-s[�s���eV'��y����N�Em>��ub���H����K>�N�(]6�������Oa����$�a�HQC���c���1�����:Y����H!�=J��v���<�c�C]6J�6��Z����4�7JL�s��n�|�K�_s����;�T�2�r
D�~�A�Y3P�5�}�1^H9��A�LnG���!���c�fg9�#6��9�;h�K
I{�$����dH���fM{�)��5S��,�N|�Zh��pz��B&�g�9`:L���Z��ZB�#g�;v��VB���bM:/��������|:���v�?���8��y&)�Pf]l)����2��Z���1_�,���q�
Cy@��< ��>���a��3��O���7��AkJQ�C��
{*�B[�=��Mf���j��Eb����]k�{)��dG�`O�\f�:����U��\�W[�O�;�E�N�n�"���k8�2�BQ��5����)��R�V�s)L���/�ss����#�R(5x����"4��=�b�R�n)���>��H����RJ_�T�R�6����RT��HR-|�������ghQX�14�*P������N��p������l0`�2
���Cd��KD��!2�Q���o29���j:M�5o� ��hH�5MzT��sXh���^j�&�B�]�r��r)����`+aU���2Z��nI9�}�T�����6�������04�ll�R�O�Z�@<BQ�*�	C�:?����hm����BK������L��A�<��k��A���M���R���,F�&?s�\��(ol&���m=�W���m���R�B��e�t�����0�����N9��TBR�AME��9!�\�p����wJ����2R�,��7�@���bX�<Xd����v���P�T�}�s=k��U�������p^
H�@���������U��
A��}^!��B��8����U3����T���%�Q/k��7�
YMT���p�i?�R��A,����n�CJ���=;w<���)�%�}6�l��R��$����isL���w��H�d�Zh�������TC)�!�->c���!�z`	��������A����KTg���^�=*�=;��2�����Qm�OX
�f�����$����=�=V�
��N����1L���h*�z���0<,�.���QZoU!�O�w�~�hy�s��6��(I�f��B�K���R��*fl��p��	���\���-k�����o�N�:��$�:�4E��	���z�J82����(V��Z2����������K�b���������[��f�uT9^ruA�i7���s�fM�ir����1��VL��[?T��;���L���!l��������nW|\��y����Z�Ck���`�y�7m��5*9�z���QG���E�{�V���ffvv�e�E.B�����g4���p����-�h���Q/k�����9v8'T���0R���P�� �*$�2&������72��O�w��'�J^Kan����L�z.�W��gV(�^�=�KPl�Ku&A�S�����'����T�e��9U�u�c�|uLa�P�l��Z��JH$�LT���������$�s-~�*�?NG���k�5�s���z���^����}zS?H�*���������	��m��$/>���H[39NX��7�i=���j�&<K��u�4�f���x���m�>�=\6��#j��hL5�h�|��%}�NX�@E��������-ov��$�v\���+vE�����S����[vJ��0!
!������gv���D�r����������GMw��e�Q�����8�a���*�_��$��MG{�3�\e/������v:]�`>N�@S��-���k������|u����w���2���>�]�R������L"��P�h���P�>\����,wB{�-NS�xc�I��r-j=�-]�����h��M��r��hX�k������U��vn.{��0K���~������
���#v~�J`������X2��H���%��h�\���n[��NZ�A7�4<1�7��u�����m������D���^������_w�^�K�r�Ct���>��7�v�`����D�jys:�=*��,�X�|-�xpY�p0��q�U�/���y�R�je�m�m%�um�[�~�xXBl�����.%\K���ann;*������xV�qT��RM��a�G��1��~����2i�����L\}4q�v3�Z���l����Tdp���g�V���D�Oj?L����!�&��[�w,{��i�K
���|��A���<�'����l����r�����M�Y�U}�T:^BM �Jh��L��}=M����[�!n��k�(����s\��ag���Jz"��T�E`�N��g�E��%0�$��N�&�[�p�/]�fDA$I��\�$[��P�����j���a�<�q�
t,�z��Q�\��4��>�����I	G��p� ����
}��~������.:N����)T���W]/l����rd�`���VY��Q*V��PohX��K���
_*gQ��r`����(*�����(*6}uC]/g!���EHF�ky��y�#��%��MoN6}�xo$u7������Fl4���.0��m
����)����OJ��#6�-I�5bs����r�`��xK���{���6(���\o��~�\�nd]JOb��o�+�'�- �`]�wu��+�@Q�}���3���k��Q��E7R�������<���!��ux���wM���^*Us�Zj�^;��~�k1�Qm��P��%%�)����Z���!�a��V24�cw
�j
�[�o�,Q/����8������=�B�Y|�]���r�L�e{RaA�����,�R�M������M�m5���T1�'w����xr��W'�/5uq�I��t���Z}����@���>W�m���i��X=�r�QS��{:���/�C�%(���������R9I��!#���_��U�n�������\/��T��
�M�X�]*���mr��~����y=^�8�re�I!V���ta�+c����z�p�������N���mJ���s��+�'y4#'F�.W��Y'���2����+�+��U{�CV��c�����u�Tz�C �L������>�h<_t���Z9�8CP����K�V�^~������>�������	6}4a��G1����t�6�|WO�����rS`r�x���F������\cL,[�Kb���r�.n����{q������;^��;^���q� �`�\���q��j���6�v�y�N,P����Z�\�\���C��SN7oxm��@zT���w���/UU��X��{_0�W�u����'�%qx�Nw�.�	>v�����2���7,8y���,��t����u��_��j�������G�����o�� ({��&�8�1V�h��9�����$�X���Z�����K�a9	#�p"C�*c��������A��*�����H�����qT�W��1RB�������"�z�8����h�5�j��F���������F�w�r����3$����I�ko�&L���e"��&{�����5[�w�C��n����pyu]�>���z\�c����{��=~�c���PO�1��O+����DI���i?�i�H�V��-j?f�b�[G�z���PExy������Rk*���-��]�T��J�v����h���s���v�@�jc)���*�Il����*�����d�q�A�����ZM��t�����aX�~y�5��������sQ�
endstream
endobj
125 0 obj
   11094
endobj
123 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
127 0 obj
<< /Type /ObjStm
   /Length 128 0 R
   /N 1
   /First 6
   /Filter /FlateDecode
>>
stream
x�342S0�����	��
endstream
endobj
128 0 obj
   18
endobj
131 0 obj
<< /Length 132 0 R
   /Filter /FlateDecode
>>
stream
x��]K�m�m��_�'�-pQoM��\d`��� v'@�_�EIK"���<����--~E��<�C>��!6Z��������7����?<~��|���������1@, �/#�1ZoN
/�4�����_�.������������O��x��>������o���w��B*�xe������/����~�������S�f���)bT`T����yZ?����2B�������|�OoRDg�w	��6������1@��nX���p����H!��{�)7��p���idN���H��������?������M8��WV��Gj/�d�D�^)
�|o��s���n�y�����hx�V�7��t��0��7��D�tPF�2n���F��2���m0*�����L��`�I��e���
��j]���1���h+�<�og�o�]���Gy�4F-����)�p��J#	o|1��/nt�,�����]�'�6?��������.m ��8K�(���S�F�'����6N���f�o^X���U�ajz�F����Aew
���(P������^Y�|h����L�	]A��8z�8�~��}����H��nuZ'�����,� @��u�h�{i�Ln@'����'Hq��	�����N(�_g
�t�����c�����n�C�7������F���5[�X�Z���!�������
�++2�uk����H��;������|�F+������p���gQ�� _��U�M[E���Tn�We #�|�M��� ��d�.V��i���x�7`�n���Q������Y��L��#�]qh���=�5����cc��QB��5�2�4�����sH�Yxy���(P�c������_�"xi�����uZOr�BY�7eS��<���8>J�`����7/U�
��@��d#�A{�g��h@�����e�p����vR�i:q�,�G[���]��xkV1M��Uc�������T���6�{����&��'��"c�����|�#c,���pa�j:��Kp�G�C���c�@?�}'p������D��(�o	J��:�H�Qix��#g��7�j���X�M���?qv�����,���v�X 3��B���A������+��9:��14%|��P��5zoZ�"��
r*�4
���[_�)J�.F��=4��-P�c���3/���wB����J����Z�ZK6�+W�+��>���}hJ(���@q���
Gx cR���)��l��0	��R2OS��	�����~�^v�(<��"EL�)F�^��d���|��N�<9�d'�XL��b�U��V�MQ�t�&g��!
��}.
xl:��+7���R�"h���F.��sn&2B�W�F6���?�/(�QtN��'�Ln���L����Z�1�0m�����r���/�|����W`��c���@�G�olN��	���-{��
k�^-������o�J�7�2]1F��Jw�_����:^V�	p-�dx)�$�yt��d�c�W���H��V����@nF�i�D�n��+�#L/cO:E���SD��1n��rD}�\�i?��7��w�v��inq405LH��Q/��H49<WC��$�+�L�=�G�:��2�����xb��N:uFDO�����b�p��cc��1xn}D?�'r��L)�W�KU���S:�QMs��+4w��+b�)�Iw���t��F�McC��UL��m�����T_��6��5}w��8�eM7EN�7��[l��F�;F2F�ijDM{�:
k�u�H�bC���A��K�:d(u��s�u�����h�7F���mX3������,~������-7"8g��4|��i�]��}����4�L���~��_��K���H+
��Vd����`��)W�!t�����0,X#����W���3�����|��a�C��R=��q�3���i��s�at��bkaP'��q����>�������R���Cs�� /���[��?M>V�t�rZ�Dm5����]������N��8>����|�z���Q�����l�X����G6��s+��f�K���;�d�����B���m�jS���qx�i$���hW��]5��<u#�4��N��0����d����"�v:�(pv�L���	�.�x?���mT���#����44�:Q���1
�L���&�W;#k��c7�OF����#"qA���^�(�p�p�s��d-�����C��@&�x�Sn���	rU���$~�pdSd�n��M�hr%[���z
V*LMx����kg���]�4�N��_����wB����yZ��7�8k���3�gc��d�*��U�1��;��������<��C'�s����~�X�f������3��v&/Wk�^�\^���$�;s�0����"P"�_�]$��GhL1_���"������(6�8<��B�F�i�q}1������eL�n�bi�&���&��aE>��R���\U6��t2}1&�T��2Hk��M���q�+$�i�:� ��=���	x����ky:Y��U^K�u<��@)��A�i���1���(:9C���O�J]����'�h�!"���t|7?Ff��@7��<�8M�������.������^^�r������]�d(;N��?Fm��u����Pk�:�>���Hg�8�U��>h5yp��W{D�F����e�d9a��n��B����Zl�O7���(~H��@�~�	x�Id'��4i�h�"Q��V6
G��
�m�K�u�J3���p-A=��q�8�xc�T����A"G��H~���sVK+6m�x c�H��V��h|�:b$��yoxu�����m\J���	�T���=�H$�/����9H6��R���RB$���>�����������E�V�W�d�����3�.T-;W�H�zd���u�#G�u��)�������H�b9���&J~�
	DN���e�6V�`^�
o�0�K
�n��'[�)�F���j\����dj�������v���L�bQ7F�;S,��X��t�=��=��a��k�vM~'��+m!/7��������w������s�F�j����9�K��@�~�u�r�v<��|#�8���q55=L��x�{��E 
^��h/��/
L���0�^�����L�-<�TP��$� �?� �1�-i*�����?D�9n�L�	�r$�	5*
)�b
�P�^�p�)�G'������t�x Y
:O�����F��L���8
�����,>;p�lW��|r��W�;vJ[��Ia'RyR�����w�Z���&�k0M�:�?������]2����q��MWI��l<���%��/��c�e>*��}��N�<-�v�t:-�d����)"[�f"E�-yiL��I���+�X�f��,��T=�y7n�(!��Z�F��oG�>{u�H�R��A�5O���!;�y�"������
d������^Ut�.v�J�g/|]g�\��]|e|�F�����V_�B������*���5�h�a:�T��G2�;��4:2�l����7"$�P���.3�������:����[G�afr\�>�:du��#���1��r�\2���Ko�Z�S��T���7�I�ij�@$�l���-N��{���)�@hI���y"�T��������c��1��~��}R3�|�x
�M��[���C���/�'�6�����o�����/�{H!���|���/�J�����	����mx��h�r�{�z�������AJP����� ���7���7V H�����#PE��8l|r�Z��t��'G��#��>8�T�|BG��#T9�&F�z���]�2�Hjg�J��'��47I�aD�G���D5"��T{%�6�����|�[���:���L�BA~�\�����mf�|��X����W��I���kt��"��Q��n~y����'d}������G������q�# L��>���������O���8��E��h���hc�����([�Z8�����>�k�[�����n��G���_�0c��#;���v��G�1+/���\��Z	�fl}t~_��Sv��G�z�./zO��3��'�^�r��3�C^AGh}t���'^{t���	[��:��������[���k��Xe��9b���.�����(j���:-����=��
�;c��3�k��4���� �^��3.h��������:3����J�U��GG��O<�I��s����N�G7�NXflyt�Z��n��GG��������>:��@�0�o}t�C��3�c����z+�u3�<:c��1�G7�Vh3�<:�9J��������h����GG}N�-ANcn�����t�'g$aa��GGI�b������������&~}rF�������Q�S�8�y����_��s�����a�����l��~~����oJo�y���
���c��������m���z/�3�<:�*(a�����z������Y�����[��7z��-��cN��h��m�n`���o��P��Q��4����/8a5����(��)�����cP'E6���l��n@������x���7��}H���c�_J��6���� ������o���]��<>���i>������O������:�x|�����;���Q=!����S��*�(�WI�+D��[x�(��h�"�J�t�oE8)���n @Ck��c��Z+� i,P9�c
���K<�T4�u����d��3�_%�����A�F�?�8�S*��
�����;hVE�i���a�&V���C��QB:�
��T������wc�|��2#�R��������T � x��#D�#�I��Aelj�}
���z���B�+�g�[)�}���;�d����������|�����]Cz��L�~*��8�\#�������`�{��Iu <��!������<����6������.�R����j����=��V���>��B�B�F���TB���6>��z��V��~���2�$�����,B���Z�A�j(?hH7��:���K�
��C���?�QM�Q���IH���z����i�sB
�S�^&cr��O��B�>
m�1���O��{SL0�?��l�#���?�9)\T�Z��#�}�� |lz�S���K��:M���9Xt�G����:&��O|x*B���wC�~�e�	
�62����"e��v<>A)�}�{�im �79�y8>i�F-��j�(m4bl�h��R�X44[��2�6��Q�mS�����A�)�1L�T�������H�.���g����N���!m�f=�9��-CLW �>������>>8�TNHC���=i,)�,�s����������q�4����j#�/���~�(\�����z������T����U�H��i�\�bS�u�qZj���t<Qc�����d1|[���vOE^��r����d.�_"���|��V2��M+�FwhxV2�h������d	����>;�z�O�sv�Pt'��������W���.9�[�Mie���
��V4t�b\�]Xb���S�he�}�"���@*�G��u���gi�l�{�R5�-z�-��	��;[d�;G`G��":�����5������]���Ky�3��$�����;�3��FF�:�.���(^	g���H����U�
��)n�7�����T��S}5�HX��_5�J�����f��+�T�Yl��Tn?�1+Y���������~�_������lb�H��l~�B�*��n�MN����P�TE�_�(5:���H@����k�-f�C�}�q��
�T]��|�b�������b0��3��n�@��b�"���j1�$M**�US���=L&��S�a���:���J�����TAo��K{�9%;���5����������0��p"��I�l�����~��|���7::	���Qu�x�������w����?����{��������MUw�E�t��;��D�������m?�O��������X����`�w�j�,��h��K�,KvH�B���Y�������7�b������MT��[�(�GJoR���8�a����VL�����%l;����is��A��WUb7i�}|�a��}$���n����F����%��`\����QU	����J��Nzm��V9���\��kn���I����)/�%`�C:\��7zPi���(|_�8g�.n��Gv����k#^W]���u���)��s\��
�����h{�T?����OUsa�:t�5A�E���]��n������Q�i��M��/��$Fakxc'���F��B��4�k�n[Q������~')rA	���� 6m�����,��L��2gM�[0zoo�-�p�p��D�l��j�B�Q��x$��u��&�\����o����,�~�����Hw�^���x#�s]�p����Gj�FI�qk1�Nwww0Na�wY�ti���9F�`�O��
u������������	�������)�Q���)_1R�M4_��o����mn�w�������h�g�N������Q����)�x:%���V5%����Sr'K|�o�G��+�v}����v�t�N[\�\�m�%.d^�~���7��.qQ�%.��8����D�hweiHB�v
�P��E�5t���U	�zz>b�q��@���6�d <=S�D1�*��l�me���Oa������*]qDp�����-1��4o�o:��9��!��������������6
���!� k�A�R�X�R�ek�������#+�$��rk���
F�M�Mle&���h�	E����������	���U�&�-x��l�~J��Xu�"i����J�E���+3�4�ho��sbZ��DgK����u{I�[�����>
���z�������Q,1�>���=+i:)T�����}w>�%�t>���8Dy�H%��;C~�V�N��z��� �lV��
iR�U��PXT�PV�Vw��%.�&��Wi���9Gv��xa�8����Ri�u4�Pi�w�E������@A�[��w��diqs�1LH��������������{m9�aP'���`�;�\�����)"	��mq|K���
"�np������>:���c���9�����d������������[���C�:���a��j��*Bo
�������z
3t,�,��d�qT�v�O���E@k�t�s��~�	�T[��u17,�L���@�y4�N�3�2�A�L5�w�/�]�FG�>�;��(��z�e(r?��p��1m���!�[rZ�<��*tC�J
b/HU
y�Q�������f4���d)�|�!�0��m��C��D!g�vG������vB�Rk����7Hf�U'8,d���k��>������XEX�T��1�I��/�fI9.�
6���UBn;p�[�K \2�WC-���� ��T]����y,�q"hD���9G%���9G��25�����s�S`�����_H:�
%�R����i�s���f�F#/m��f4r�:a4J��
[wV�=}��mu9�z�����c��a����S+C��k���Y	Gh<�D����R�lk06O{�6��E�V9{<��k�:94)������s�$��\��`^��BKaO�:�y�t�M���}\jfk4��6:����bt�o�r����z`X����i�w��iBLPM��b9�V�M��}����vw�M���v�������&e�9��K���Y{Uo�s�C1����`���bm.9��1�{�y(��u ����<����Now�~(�(y3#YSc{W���J��\�@�E�}*;#n�0�pR�K���[�;��*_�B.�v}���g�;I;��F��R����I�."aRp&{��M����+&�	�W�&#����MGG���9�C�	��� zd�R�l������������a�eW�'��o`/�����:�@��+F�����d��pD�$����$�5Mv�M=]Y*��`q���[��K�����B��A"W��\se�t{,u
1m��L�\��?��l^�O���-�A����]Z��?�.|?i!<�wC���X��*����0��l"�
��ms�.
�w�v �x*g]�Io{*7����6R�b�����������~�_�N��Rf�j�mq��=Z�a�[��YY�8|r2Bw�c9������d���;������t:g3Q��b4wl��_�����f��^�%nx�����Dv������Hb����L*�������}�m��e�\c�lr����!5����*T�Z�e$T�;�Z���,�t�A�Q�Y���4T�N�����R�����Xw�=<�/�����0*<��
�c{���}���h,ys������ bQA�����������
9�Q��#QOO3F;{�<]�Om�1��RkC���	���tz2�xz��>L����>��S.���aqv���*��A%�$�>�����1��(/�R�f$3�H>:k�I���WlbIq_��)���:������,�*w?`J7�-�*ELE���`8�:D�S:m�I���������&;�qE��a�]�@���>r:4QT5�"���EU-� �Q��J��S�	t` R�C������*��	��($szgon	}N����X�zuh9�v0�p��Z��z��TgQ��G5 !j�5�������/��G
yN!Z=yRGQ�uXE�C����=�z(nw����'��<���<��.�Xn&������cm�]9�.�����YUw�{�J<���5c�R�G�[m�Vp8&c�@?��|2�������39Q�/*z
��1�ac��Wz���!w����&���U�UT��r�_*�w�r��4�ib�W����h��N'�ub��r(��*g��������B?~.�z�%�R;N��&��y������#��,�?��)9
XB]��}�|��h_
B|?!�� ���%B�Yn�~�;GL5��Mt8JAH���o�S�$�������S�e��ui������}T��]rrp(���&KT�����i�k��a^����=���8��xh���/�
��b�xm��]N��;�zG��"��57� �8�q���������MV�+������~�����[>`�x��/�<�fRa���;�c��2j�_���'�<���4j��wU�#n��|���Z��I�6���F�c%��8��6����jxd��J��i��?�H�!�Z�V'S	ll�~#z�����5�_n��x�5Vp-�m����F-��hO���{�<�@�T�\�X+e<Aq�(�p���G�En��b�2�5�v9
��{|���yT��?8�n�j�O+%�[��6��������^���e�R)���KA���n�#,��q�����n{e{�{�00������� �<t�����k�����Z��xy��/�j���me��-~@�W[``�l�	�Cqn�.��j����he�c�p��ESo�L������K_&qVK������I����;Gd}�?*�+�-�p�9�-hG\�������?�-�rF>m�JIE]��P�����.F����|Y���;^[��X.?�|���j��v�Y����%�T.�������!"�[b(G��>���s�o{��%�;$�`�'W���[3B�ifo^���=4�-��b�~Zil�j>J)�b^�m3q�$�tx	�����N�_m_�'�g�c/ztj�O��]��Nw�;��\/��{F\����c�i�T�V�J���t�BX{.4���n��(���+�4U@#�Z�����E��C��)R�Y�4]��]Y�q��
#�?o�>�Gd+�ENI���W���/��9v�{E��w{�k��y"�s~/X���t_�/�P�mY��b;i�v����$���D�+�9u�JG����%>�G����~�������;�dPn`�����S�nIN���[_���4o\���C����rn�T�Wk[�W�z��� ��K�m��Y���[�����0������bcN}��t	�`rp����_���6�*
��{��T���2����6���FRCt��',���1.�~\�$v9�.�]N4d�B)n]
���RC.���r�e���q��:0��!(������58����L�\�o_������^�Lv��(7�������G���
^�|
��Gar���~-L���W��~
���Z�B�t#�i���z]��e����o��2�����JGY���~�Y����X��\���d�mH���������U���a�M��R]��_�*���
x����)m�����I�B������9;��S;	f��i�~p�����6��6>���|�iV&N���+���@�4@��as�@�,�|c�x�B�z
����Z8�pp-V���8f����R���p���������Ru�&�|�@�_�*�������l��}�'�K�a���[t���89�kC�F3C��F�X�cO_�R�5�pJ"��Y��}�	��b�\�S��"�K"I��W��F�P[�R�S���,e�%Q��r��:��H
�ho���{���4�q���)��XM����������>���dK�)�����:&�i�EY�FY��_�J�Z��7�	����H������;��%������4=}�������G
endstream
endobj
132 0 obj
   10734
endobj
130 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
134 0 obj
<< /Type /ObjStm
   /Length 135 0 R
   /N 1
   /First 6
   /Filter /FlateDecode
>>
stream
x�346V0�����	��
endstream
endobj
135 0 obj
   18
endobj
138 0 obj
<< /Length 139 0 R
   /Filter /FlateDecode
>>
stream
x��}K�-�q��~��85@Qd��5```<s�Y\h��V�-�%�%��o�$��`��kC�������E����P���
��F����������~�������������7-|�A��D��z�pRx��q�/?�����!�����������_��n����_��]?��o?���������C�:�����>��w����N���������1:i�c�����y��~x#t�0B�������?<~�_	n�2>��3��?,D��0F�p��SVxkX8(km�6�QD���7RH4\ej��?���=�Bi�	�p��.A�+H��p�L�KC�Z�>5�c`����@���r���3���^��&1Rh�<�3MN�Mp4���5�1d��&�1N��'��3t�y�����N�g��S��\pJg�h-�(?N�m���r����%+�FK�d������q
W�r��-n�G������1��Xf4A1#�&�
��%�J�����rz�j��kR 4����')����10_� t$�����1���(����A4���9V���A,Y��5a�d}z��+h�x���)�����������I��]�I�#|^[���K�7����75��yW���B�*��$�R� \ Tvj�7�R������7�Y�,��#��
>�?FY���E��@[����i]R$����:�l���(|��L�L�������c0
�v��.����v���a��%��JBG�b5'�{���EE�O��^E��(
�0�fC�
�L�4=;~>��,%�V�	�$hx����Q����d��xcUA���Q�#�$H���3DA:�2���d���?&A����"�&_�����D����92�C�R���k��U�� ���$\�$�
�����	��Q�$��X�y������G�����Q��,�E�eC��,FiPC������� ��:e������P(4�]�lf�-��T��"�J����FoU�G'mY�Q��jnJ0��������#A��92~�'	�G�i]�����Hb�)bp�u�����]T~]^��.rr�&w��q����Ps]�L��s�H��Kg#�7�I���$|�>9����KT5���L�9�0����L��s�	����6��+��5���o�K
��e&s���d�B�g	wQ���A�B���r��5;3�#��"!��HJ����� }�^y�q�T�
9�@�@6�#�'_��kE��r�����o�K
���d:W<S��<��.��iZA+�C�<8���1�L�.b�&�0m��6����z��Q���m��6�q/��E\7f�$3
�MR�at8�
k���uA���$H{�4P
��e��1���%B���2c�q���m�7q'&��PR	���T4
D�0��.�Wg�R ���A+����*a��<���$7�dlAY�0�7�v,�IewAR�� ��]b�dn�$��L��0�X7�����
^�Aa��C�����:����I�mo�$������b��|oc����15�q�ox��]��������f��q�[7/�8~��.r2��n�A|�m:����\��7�� ��.�(�&���1a���f���;}�4~�j��.�ec����]&�Yt�C	s�O�q�n�(�YjBa��C��#���Nw+"�O�3��@x�WQb<p�E���Ub�Pb�o������
f
�_M9�����k��NO�MD~�p��?m,-��%f�0���9�2��F��NM�|�j�H�*6sv�JeIX4����(�HU��y�io��R���3��VQ~#��9���bK��%��8�O�I%f�P`����t�cX=>5��$|�v�P	3�;m��dx�|�0�d��������
�*����O��L�"���C�����Yzy1d�tOw7P����������H���c]�f�������F�����������F�D!�m�zE�}�j'� ��o���_�9������">7�3�3�t]��8Z7N���"u:��f-@(1O75)���s�(��R�S~.z�r����'�	0�H��VT��(�!�A�����Dj]�,�|\�������� )Z(����Sl#!���Fj$
�p��m�`�L����!��a$�Y`��6
��P@v�����d�P$0y��^���E���VOw���X ��(.�P��<-�+��F9FH�|�(Gx]L�P=�I�1�;������q�.�J�q�l�KfeEO\�"���)��@r���.
��u������g���m�?��xz�4�]^����D�h��t� ���C��	����i��V��R�c*�qx���I�������Z
�4{���������A�+�����t9Q#�xY9�P����A�;�9
d��E0�+�������D�H�;.GI��{p��U`��%����V�L%8.;I�%�x�b�s:���=�]�4��S���'u���!���������$�4Vg��q�+s��:����XJ�Z� -%9EC�XW9��I��7��X<���,
j��,&�Zd���3��t�C�L��ur���97� �{�%�MD�N�D0D��5���(mp��F�W�2�\JM����R�q0����$�Pn�w<�~�{�T�^�`���h�""jh5�^��dV�%��J����M��$�1%�#[���
���1���	K�q�������j*���*�lJ�?�����0@6%3�	��;�^D��qS3@1s��7�2s�������l�V	��ftT��:x�9�pz���0G�N�fJi(�2�/KE���d��L�0>A��c��},]L���eL��0i|=Z>�@��W]���We����u�}$w�f������w�����89��o4����,%=%F�@��#�M��o���&�\^<���1��]$�v�R#�\r�8�.�a����N�S�&	o�e}nd>5��YA������xe�q�(j$
��l�O��R��������ex
����4zDc9��q4^���\���s����^iAv�@��"�C�Z�1��~:qz�1�$��t�����v�	���IbCh�c��(�2�y
�f(vo�m��!#��sl��Y,IN?#)0&���n��Q��{Eas�mq)������:�%���+/���"X����I��#�EX��T7G.��z�9]d�(6�}���Q�w���V�q��NK���p�;����6	xD/�	x���i�2��w���bw+�f�^G��x�	���u��)��E�F�����;��}`�mF�E��a]d��C��F�q���B�(�7r�,��p�0iV�q�"Kdw�~�:0B���Kp��#�j�k��_77���G��^SS�DJ�I�
U��n���2�KTY�.}a�	H
 ��0O6�Uv�����))���">��9���=�&Mf�����Do�(�~25f�M�U�V)a����4�V�q��vO�]��}�*(��%Ux���?�4	.�h��58�k���os�?��IH���?���wo������O�/���M��??��P�W�d�q6��������6<~~�\���?=���_���AJ��x[���2�h�w�@%m��l�@���8m|r�Z������>���rY�� !7lv$>I��RH(���?��;�9d��4�:HD��#Q��
#�>8�&��lONP�Axm(>9A���:��'3tX����8��\����dXRp���O^��w
�ey>91�6������&]�"�m���'��k���'d}rT�`�
���'���&!���O���H3j%������1�fdyr���NM�m�8���~G���<:�UZh�&l}t���KGQ����L�H7�����(Y^D9��>:�Y��n��G7�^8�3�<:c
3�q}r���B����i�����|��X��O����uJD5O�>�v���9qQ����Q^�]��GglPB�y�����Z��qk�n`�p��-��X��������Q8=c��3�D��d���3�Fq�ryr\�f���m��T]>�[��>���q��8�<9"���0�q{t�*/����GgljI������	+'�����	��n}t�	�SB�I��3]g��?a��3������=:Z����������n�`�[W��V�0���n��)�;c��#�H#����y�*�f]����0o���Q�D!��O�Tu
gh}tD�v��Gg>-����>:�5A�y��Gg�V�������hl���-��svZ�8o}t�E����G��� ���M{t���pn����L7����o�����a����<������y��%/���H�J'��~{t��rc�Ot���NZ���j��Gg�5�3bO� �^P���'ly��������?|y����TH/�x6�=�a�J�w}����?~��P�/?�}}��tx�2���_~���p:8�&�����O'����x������|�(��DT��XO%�3�_������*���&�w��/�������?�(c�S�i���(T�l}#(������A�sSA�<�O%�Tu��,��@
	��isS�g�8$xs��I>Z�8��g���kOo��g�	��8-%4�h����(t���T�$2�i����1�B�[|L/0��uHV�b<�U����i��O9y�^���F(D�{�+!�r}mB��SI���4�F�GG��4��2���(���f�!J�<\�������_d�_�D���~�]d�Lbzp����������t{e�	O%�����T�iJk|���Tu�B�*�����M�������v���S	�I$6$���t�EC��~�X���D.L����D(������c:g+g���L�3���0��1U=SFK�A=t;`c�+��Fy��VW��h��S��p���.���(</���j�@	���JS���`��L�H�����J��_H[�o4|�)4�,�R�4�
�����i�3����Z���Y�!>�FeP�P��*�23�#2���}UVcV_W�@��dn?�q�p�}l����O�;�V����j��z���S���X����
W6>��n��w�:^�nL]P�eb�3��	k�T�N"Cu_pW��+�rU���r���y�m�KY���w�����������(����I����A��>P�C�xq]b�k���1I�-�����h'���V�-B|*��Z62��8H���M��M-t��*��8���5fY��^xmB�&!|Z-�!�Y����m�5S��E�,~�I�j4���3K�M����>$5PF�NHadw1�i�(`\��[om�����������g��
�� %Z�&GY]���S�a����p6����YHi�f\�	��Wp��j	��BP��2�����iS�)V�N�)�~���l~�v������2^>����v�Z)m���B�����6���o��S��
*]��+'����L�t�a��.��6"l����"l�>vS���{������qQS}��j.������2����a�&�3�
�p�1.v2����$>��m��Lw����B������>�#�:��	{��H���9I�z/�qKaUs������&B;e�d,����X-����d��2XC�+��at�����Xa^q\����uZ6��+X��W�}�%��/|s�jW��(�sDS����t��U���T��t:�\g���J��E
>��C���,!���L���p<�1�
IGG��jxE�`�+����B�d1�+�X���0d�T�[^`��
�w,�F�����BlQ�����I����;��������Y�R�w��P,�'v1P���+A�9��
�gd�m�(�A,Xk�e.�G�������Sv�k�;?KS~�74��>i"�9%�
��k��
|��Y���C�����c�g�l����L�A#�J�~FY\�f?��O�o>����������'����;k'�<s��������oJ�G�<A
6u�����{^�����O{���J�|�KfC�:������������_���������
�_�cS?�w��t��Vp����[��k���U�{Y���?!9�],�B���H�y�o��6���O�<���#�~��LT{��#t�~����"L�����W�)��)a�o�s[������G�1�j�&E��
�@�P��I�����J�u[����O$��	�c�"���Y�����n9�"�s��~�E�C�7(a	�Z��S�&��<uO�\
�������a�)��_U�7%� %4��(����k�����Io�,B����i�'�n���%���V�tk]U�)�U��a���8������j��9g��F���%�s�T;��&T[�����{G~���?��p5(`��&��FH�{K��Hjw�PoX�H �._R]��-���J��W�
��a���~�G��Q�=�n����M����%$��Sk�k������*����18z��D��h�)���gcs�Zb�*m+�ry`�\�t����&������N��Hkm���mE����sr��Nv0���UK��d��^0A��j��Q�y{�M���r�-����Fk�)GD�;E4:"�Vf����,5�e�s��x��fh�X�u3dx���O���W�m��sl;[M�vOc�R�����&�%���i������tI����uCa������1�`;�0N*9�mF(�5��N�������&w�&���vcLN\�)v_����M}Y����&�F�$0V�5S!2B������%s��h���F�j�,�6���f(����}��O/^���g�=::����9FT������\��]@���6�C	��LT���\���0���w[;�
�����W������V���F�
L9"�
���6�f5b�)P�	����F5�R�U6q8�G(a5z9ch�����h��m�-�~4.��^.#RV#�*D�yJ��Wj���cX��`�,��*J��U6Kx���y��	o����n����v]��v�.��=a>\����@����sc��igK:��z�N������^2 4�C�[��S����hs*o����r��������,Y��o��n\s���$2�r�Pq����=������b�+��JO��1��ka~����I2��}O`���-l��A7pob[�i�'b�>f?�L��X�{������hBz1xcBZ���c������R����G	���V�2��T^Ob*H$,H����2�s�JL~���#^u��\�%�g{9q�mk*�lu����i&�L��u{�mH��>�>���.��>�(�B�x"N�L����rC/��FHa�eQnd�K��q��K�
9��H�'�?��flY�kW�t�������k���b)���~��*v=}��������+�>]�
I�k�u��o�7]4�Z�}s��m�Y+�l-��tkI��9���$Y��1hI���::S���Uz�ZcjQmMh����p�s]2/��(2�5*M�j�$������������y�����m�Z�L*�zY��8�yM��'�gae2��*���?C:�����~ cBK�f�f>�6��nL�Q�Tk#��.��\�����ImZg�b�C�G�����|m2��2A������6��L����}�\�T������d�j��@=f�w<��

�I�u�����d�^���D]NZ���FCy1�z�v��c:�:�+�����'J�!�����t�x��&��+���|��d:^��_P=M��u�z��R����0�37�$Aq7UB��	%,O�����v�����4�IW~����d���{���&��'�-�xDns����Z�Bm�p����Rt��:e���*�R_K�����������1������V��������!v�����E�#V_W��r�^���{�����F�Y��}�n=Kn���XT;��DFK"��&�K������q����nx�i��6������s�R�'�	@���������iH@*tM���N��iF]�U����)�M}��y#�nr�����R���vS�C��>:���A-|�Q��4n�@���O�7��z**��������7��I�p`d�{$��9,<D���tQ�u�v�H��`����K2#X�\����a(�.24��V�z/�'�dy��l>D=�l&���I��%�e�^n=�{e=�Oo�X��1-Kb�t��R��v}�^p54������e�����mw$��aq���+���$�D&���%������z��!������`�&N+�0��U~��KH�6����v���aW�6=Ip��n-LFK0��la]���]���:���K�q�[l�c.�:�����%�*�.�uL�{�fJ�rJWN�h^�&�{>]��%��M��%i��x����;(}���e�XN�Tf��^���Q���^����&�o��dx��!�ab�o�0��� k:P5�d���o��p�������sW���Z�L� ��1���P*�e2���fw�z����b\j�j�������'M���uim��v�>�^�P#K�����%�k��!��
�:}���|(�kwJ��q|����~��A(���\�zA_�LQ������#����R�a��k����{P��;��\vOK�+i��3���� l
O<1�LoK�_�	]$��<9��,w
��v[���1(o&������^j��L�Kf�M�)Qc���f��[�C��z����vw^�1INn��V�����������z�%��shs���\�z���
e�m��k=���ls����F��KpL��Ru@�M�\�!�M���U���7����cF��&�F���un_G��[����G-���Q�Pf<��1N��=;i	��_��=�w�
��M(�S�����i@i2L�	�S7�����o�t-ngv�	,����r)T+���(�������`�5����v^��
�T��-��rw�t�)��p�`�����n9;Nw�S��.�&h\U;����#��O���w�D���z�����h��D0^	c��0Z�=[L������3c/8[;:Lk���Z�*���0�]	S�:LH�ap�HA��8/�o�i�\�b��B���\�����������(a�7ue0���~c/��pAW_����[fJN���j�-����H7��Zx
���j;�|A��:����N���=$[x�k�������]�>����k�A���7!�|1I���_.yE;����&@����K�/�fu��Ah��8<����>=���]B����������/����e��:%r��%�����G��)7�b��|��]dP��~���U��PuN_�X���������~7Vl�����K=:�u���L8,u�r*-��C*	����F�A�A��n������t�����hJ�[!����X�TV7��'*�/}�^)���v�s�����3D���������r;��'��9��d7�ip�B���u��W3���hG�8���^����o��TZ�j�](��e,��[�=���[r�p7�������X�������t��Z��rI�c��Q�L�������8)%���������h���0T�5�m�/�~+�J��t(��1r
��|�WY��-�����ogO&��[`�{p�����2�T�N=�w�m�r
���X/C����v��~cG��~���buo&�vr>�_s
}�c��}�O�GJ^�t�����������xk�\��oW��[+���}��+{#��.��Dq{�@��f�ml0��u����{�nmon���z��^J�,��?�}����-W����{���q��w�=��&������82��>��6�X9��)]$�w'��K_[;�|�b*e�5��1�������2*�S������7��v6S�N�k��<������L5�u�-P��
��M��l�t�c#7GTJ�|�����kOmo�D�&7�],��/[���lg�����D����������_���z��{ �����3?m����{��������7������7��v�n�7DT�lMq����x�&����*]��U���6vpBG{�W����ru�l�~�S���;G�w����u$EC��u�����j� ���L����OG���4���5����Ob�e��
D��C��K?�d��?��M����w*]g6VGE
����W��k��i54[�� ����vS��/��;���}��TF)u�lma�
m��}��z/@�M;�b~Y[�K��.���kI��<�v��X��.��p�S������I���[����H��y�����px���Y�Kh`R�?�W�O$�^H������X�-B
endstream
endobj
139 0 obj
   10482
endobj
137 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
141 0 obj
<< /Type /ObjStm
   /Length 142 0 R
   /N 1
   /First 6
   /Filter /FlateDecode
>>
stream
x�341P0�����	��
endstream
endobj
142 0 obj
   18
endobj
145 0 obj
<< /Length 146 0 R
   /Filter /FlateDecode
>>
stream
x��}[�d9r������u`kt���a�`�Sp�yh���6�=������!-)B+w�P��J_J
�UK=�C>��C>\rB��z����_�d���~~��{����o������RT���_VFSr�>�Ai���/o���]���z|����������}���������<����|��O���7%�v�w��0�=>�����/�2�������y�2�?����m����p>e�#$��h+�~X}x����O���������KJX��(������^9��p�Oi��^�#��!Z��;���J�N��0��p'�+��1�|_F�(|d�)3S,�cd�N4IK��Ai���v����a$��B�����a`\2"�����j�00��[���#qR���/p/C(�H!�����������E��q��y�0ph����!~�hu���a���Q��(��k�,:c�����G4��88�N-��1���������e��^������'�}��y�u����F�i-�w$pV����s�
��W�����Ww:�z�@�x�k�S����L"������#�I�8����JG�i/'7����]�
���
���	7����6.�����]V���`h[|�1`�t���u��C�3�b@����s�Y�^�v����N<���������
B;����a�|�/x�R�P^��q4��g�9�t������J�0�o���90�i�d�8�1
;�x��I�0��^Y����G<p5��9$�?�8m���W���E����]��Q���
OL����s���v��(O<8��1%l�)H�Gy%������L�r�#Vr�CG'Y����L��'�x0�O��_$�!�/�����{G���q>XH���a��5�(p�����7�8�ML;S��Ri�\����������<"��>��
����"�3������8��Nb-r�T���H	��| �Y G^
(E�J�u*T���m��8
�l��!����q�9��y�����$�F%��u���2��gCN��`�.8��b0�;�^��3OG9	r���^TV��:�
�a3���U���9��m�
�r��0�7	���6 ��A�i�'�+���@`v17�tf1{�v��6�z�������B�c���1'���9�����H�������lI`�q1��a(a������>�����hT����-n�������_A:�7V��=
�k'����N8��j�~;$eC`�:�_�=�J�����z\'�u�c�j=�M��V��%h����k0}�	_��icAf����T�A�
�6�-�,�m��	����WN<�`������������A+�6h,98��2��MYr��p��Lc���Bi6y
|d��|$����l</XvX"y��Kd��j��x�*�WV�������s,���U]�C�9�)�
���������0�����Qy�E��8�x8r����"C�i�0S�Bs��	��i�_��8��[�����~���BQ.��hCQ�O�Es�+#�����*h��2�n�,�W'A�$�]�DF�9��0'�"����~������.��]t�7���
��������c��]�5��<:�h4W�Y���DB�s��8��t���IhO�W��n�8�s�0q�.����J2�p�*�G�}�V��*EP����'
���9��J���d��2�z����x6����s39��
8�O;�@O��Q��%:K��8����}�H�{E���f+���������X=�����s������H��1�cq�h��s�H�
5��h:k�@����vb�`4��k����pa5��R��kd���Po��TE�
��n:,�V�����	��E4�^7���+�L��	y?O���Lv�����y�1�q���a��QXE��	`��.2�������"G3��2����?������D�mSgW:F��p.�g�7�!H8l�Lbq 2��u�q��a,���|m�eK�(�&���-�le��c|����"�"��/�w���_q�������/�9���S ���,r(p���@��z!J���~^.�}J$��oC�-]�����}f����C��/�
$�qH�a(�	aiBG;3���}o?%/]�5�x"w���x��YG����/<����� }��X\g�4t�3�oCf��,Cz�4�`���.0Sq�0�������%��q���a�~���a�:���"�7<��!G���q����G�0���D��uvp��z��?�b }���v~����~����tY�/�:=�V�����I����
4�1�8r�U�vnh�r�4A�jcH\o$����x�x����;3u�~
�����e�!�|����5�����CEnP#n�?�f!�M��#�
��i?��l��#�D�d<�N)��C�Ig3�u?�C�G���;(o��1%�~��o�A���V����"*����H�!��.�k����5nb����M����}�/�����S:X]�(7�J��l,�x�;�d�W2kDm������xX"��>6/�o�����qs�������&+���u�6����!���������"v���~��3��D^�vZ���U<�i��)����@��c�Wf�s@Q�02�_�3���0�n^������3=�Y^(C��7�{wm�g~5���f1w�0f1������U�d���Y��0���������>_&v�4p��G�1������.��.���qx�l���YLR8o}�����X7o�L�T�)L>q\��s�b����g!������c��	?Lv":���qe���N��[�������xL���Z�1�nn�r��=���93�#~�X������\��S��Dr��d��S�
,!G�*�8�Z����1�O�8�	��7���p�@��#�y��k�>272�G��FF`[�����{2�c�W��C�;�*�����c��.�C� ���@�;�����
h������~�������x����:��j�=XE�A���=O\!�W�/c�����c������/�	�G�[y2����C��H��H���O,���R���� �1Dg��Ou~:��n����&������)����������?\��L��9+��(����i���K���y��h���c�pc������	N�5��V
Z��#�`� 9F����@6
�!�0t_����MYa_��1)�a�vJ��^�9����������0��R��t��UI�H]Oz��
NU�h��n��]-�m~X�@�7�
��|I��J�V[���l@:����#��1��;��T`�8�PiR3<JB��]��j��4pa5tr��W��Gk���=�/��M�`����?�����t�5FDO��/�9��;q�+������A�j���|m�9�����|�=��f���+���<���p����Q�h��(�=���8�d��a��|)��.���FZ#\x�]`$	���M|�'������<���kE�q���|���(�!�q�e����8��Fj(r��U�( �fq���2@�]M����j� v��@��o���'������h��n���[����K�h����2=����?�DN����Kp�a�1Oq��sQ(G��R���`�G�r�������z���N0��_��q����s���o���)��Y�^�^�t�P�9��%R<eN'O�U���+���):�Ca�m�9�iK�^�4r�t��W���x��7n~�����@H�uA��I��y<�a���:&�7=~�u��k�$���q�iw��_)jC.��sM�#�h�U��C��>E��`]&~�W�L���9���?Q����6yy����u��C�
�<c�wn`������<���?q�@O�-�A�DV�rd��?�RCq_� ��n�������b��w%-�{��x�A���#x�;����C�M���9~���=�r�F�k��3U�eAHEsA/��g.�AR�
����	�����gD�
����OBw�8������4�s���W���T��W��Ev�EG�pj�Nj���[�h���������8������A��6-rqGfqS��3
x��p�,}]#��,���b�Wv�s@2
���_<�~�)�H"�M�U#���a��y����W+���HW�q��u�P�5��;6o,�3��
h�(;���D�-��f��C������w�����s�(t������#�``0�|�?���jC�4��_o���7����/xH!?�)����_��}���������x�V�<�����?����-�u����z[�#�������X�J��[��:	��e��#��������nD�'��Zd�Z��4�����<�y���O��j_dd��'G�SB����	����ONP�v�Pxr�Z��{�
O�R�c���j$5<ya/d���:~<ya-�+���[������������{��3Jl{r����1zB�'������
�]����IH&d}rD&#��V��p�@:�xOQ?#����J������6
.����|����RF�'l{t�WE��������,)mD��r��GG*+D��������������f��n��G����K���v����<���������^h9m����MJH�&l{t��W\�i�Gg�N"���"�?���T��y^oD��\�Ggl4�����n`���������W_z;a��3V�����Qg�;��$W��y^��F���
�Z���GglnO����Qg��e�������F'L��\t���#��=:bs1vglt�j+���=:��R��fhyr����<k{t��%�?:cs�E��m����������G�yC�O;�?:c��NZ�?:bsD��	���z������Qk�u���f��o??����Q�������_���F��Ga��!<>��������|������<��wM|�O���7�E��v7�����S�?}��7/�O:W���_��,������D����Q���2��J������v���z�R(o���w��Q
6�������?�dIX�'�QD��,x_'���h�<��gh�[o�7� d����M�����M����0!�j�S%���j����[�s��uV�f�;jyj����}*+|����^���{&���.}z�A���mx^:��(\���|&$@�.-��6h$��g���U�F�\� ���jm��nX���Ff|��;�r�T��wePx*�����4�t�������5�<c�O���������N*������������������s	x���?���kP�
�Qm�%v�3Ep���0�H��'�����t���-�J�=@J��$���q�~UB���rqg�BF
�vr�t>���LJ��B���#Ks�h�~��0��'H��+T.�����0��x������1����3y�KO[����M4�c�"
�D���P�,-��)��3��AR8\9��?�s�F�-�������$�]Y��z�B�E@J��	H!v���U�)���EVS(B�5o�Z��Q	m(a��@X���J�6��E��B�#!��5�d�SQeq�{~{/�o��Im��������	;x�����(c_IC�
(T�O���(�~#����M���z*�N����_9ag��nT�O����c:��1��P�5`�xJ���=YQ��&�\�������������P�^�-)}~W�e5hF%*5��`E�Ia=|�D���4�*���2�hg��{��v�7�����|]��D�uNo5�G�$5����m59�.6F�������j}�}���A��.��0��\���\:��n�
Ug�U��l��'S�W������-�l*w�<~a{f�jbt��`���S� ����M��>U���Tue3��%�ER+P��NP�Mk�Q�z�kQU��Q2����.�~^����}��M�P��#�y������_�)������_�h�0�}R���CY/Tp)��s�L�?�������0���u
>yf"���	�K��z/u17���m\3�C�mV�
���J��`�?sc��������4��?�q!Q$3���'Q��z�r�q��d/ndO;�c�a1����8
1�[S^
c�/������3�N$$uq�38���nm��M��
��I6IuaS6���L#��4�7�S.iX�;i%�N*-����6�)��B�YN���1�v���G�@�
)WhS�c*�9n��4>��V�
. S�N���YB�_�u�2=�a��{K��GIv��$�� ��Z:�4�$K��Q�����$�k�dpL��QK�v����e�k��f�=R[�H����"u�ZIW���D-�����X`!WL���{R����N�������)��t\�J����zG��C�XS��$����"iSJ"fJ+<G�<8�
Os�G#4X�������y(����c2����TC��$5����H��������8'm���9�]����%�������j���������n��C���&O%pN�������e{�����UA�I|�
ZM���5�
-����HP�B���H��"�B��k%cjAC'�h3��h��}��$]��.
��k$���0�S�
##h��&}42��hg<A`�|�n+�22z��
9H�������ndhR��V&i�;�yO���5��"Y�bS����4���Rv���@ `�3V��Gi��!�y���@�,�I�e��4�-���p�;Y�������I�6Hr�'�������uE,�Zk��o[����TC�B7�a��f!:,�9��TO����p�m����%�@�jD�X�P��BCe+C�e���R|B��|U��U��	s�~��nMHh-�4���v�|���v��8�6�:�d@���:WB�N� t�*�4�5��c���"�|K���!���W�:6Z���P�R����rO�\�V}���^i-�]�m6�i�������V�T���C�k�
��S����DO�/�-\�g�����	#qr"\�V�.p�x�Ue�15w
�N	ib�5%?)������iH���x����E�4���6�=�����v_��Kh2F"p��7`Zs��`�h�kKZ�U��lZc�.|�����Pd��"�u:9���	i����G`��k �8���dsi��������0\
m":/���Y���?
1MCH�e��0�KX���Z�@�� ��d�Q�cI����X��N������F�a��a��b98�������Jnr����e��*�Z�7�r��V~1��AeH��v��S"5��r���x� �����Z\�U�:v��cg��Iw��_��"��%��L)fDpeH����o2��8��������21��&s�5�ot�*iDG�
�����$�&-�S��:.��F��V��W����6��
*���b��m&j�V��L�|��
Z�
��V��}������=���=PX}Zk;�����~ZyD����b_����wkNw�YK�B������C�V���Q%fs�.*:�����5LX\������(����	�����\��.�������0���V�O�����/	�����h��B��(/$MM	I�s,E5A���]��jgb����� ����k���;WE`V�Y.�Y)�bB�R�>��
�S�w��L���c�
c�r��/��UX�^`C�.��v��R���:��L�F*�m�Z�!�	����m���P��j3TBW�#N�}+F}�
[uR1��I/�u5�X������N�F]�5�o���D��<���l��b�/��jU����`�(���������5���AnU�!
�{�������>c��8�&ql�ZT��q�������9���@�j*�E������Ji5�+��):�)2rI�4�X���@�)�L�r�G���n�������Z��t�@b�@t�����:�d��q`��!�n�D�TU���A���y�b�vW�jZ���0�G�j�m��f5�uV%��c��G��t*�6u���!��6��lc��Pi�'��!�c3�*��tG�<��a6�,�=�\>d��c�C���h�}��kI����t��Bt����k�A�198�N��nr���x\
������������s�<�T����c�tCj�*�z<���$�U��l���)���c�����v�"�,R��$��K%��e�N���1���ZWC+����bi
�xy�C�/e�n$�gh��\�:'�ER���Ym����[�$�Q���
��|F=�1�r>%����W��O=:���z��l%f�
Lc������������j�����{�NU��^'�t������E�9Mc9�+N
_m��S����d-C�Z-k��uYq(�k����&h��k�U�	xR(	��+���"$ ����4A.��948v�-A>Y����B���ma�V���5AgY����h,Wm9�h36I�����G[
��=�|����"��5q��#�.��|d%t����� �b<�51w*uZ��#2@g��A���&�����0;<�n�������������BQ��\?!���2��L=�R�U�|�����I
�JW+;E+ag]��A����*kK_#�|���d����C��D���
`T���^�k�&�u����e�m�p]�#�A���g�o �{�!����z-�n&?vAO]�|���T�P�,���q�-�����hjS��P��BMN��P�?������w��q#XHSj��1hFR5{��'(�� ��
A]
��������?&[.!�8u��b��j�����
��Q����r���)��0�B��=��4e�(�P��<4�,�nf�z��	��^	U��\�^���h}�;�}�6�$G�����
+�:�u�|jEF�\�u��M���Tk�l,��L��15!��^!k�3u���Cu��7t�
����{:����b�Z���4/d�P��Z(B�G�)�l��*]�.`!n�~W�zK����G
�����zQ���n%�XB�c�E�� ��W,5�����
VA���Xbm!���f=�bI���+����jq��"�v{�M<+��������f�=�j�O�|��`��p���5$����2��J����.�x��#�B,P��#��.i�:c���:DSbx�����@�����
s�������m�*���^4�����Hh����(ly����|�S:\���c���rm�����%w���
�-�@��HS����5�e(�(C)����D	G8�y�i\�����t[���`�R����X�C����6�����F'^�~,�/*k���!����P�d�J����g�0�g��L��?}1;k|.��_q'%��q��n�����1��q��|�z�)\(Rn4[��v'���`���&HB��]���X�TA�06���
������T��������5(#wl��}Y�p�����/�V�RW�
%�*�]����}%�����vk	}������r9��uz.b���?�UGM��;���|[�Cq����E�n��������S48�=���`��#�RC��,��Gz�V��k��O��}��+:l;D��p����,���hI\s�o�u=��_���:���r�V�����J��
NQ�������C���/_�uXKQ��z�>d��
��J���*���	���l��V�V.$*e�K�Z�V��=��W���;��;c�j���3��7���D9��7I�m�=�P�:{xP��".j�
���SD������WA?Z�-�R��H�q�6�9M�B��R���#Q�];H@��$r���v����%��a�c��!4�PoS��NEm�pjS���#�T�J����&}zK��C'�A[{o����a����-���m��^�=��S�)�8 �3r5�n�r��Y��-�dW�E�1p����������\OcrY'�����l����i��lE����l�P��q1k��t����1�k��K����C��J���4��f�����
:�.�zb��wjVP�w j�s��<�E�����K1�����xsxk��\�'�L�^���s��C�����**���q���N"���M�������l����9�5�����w�%s���s�{�x���u�F�S5nsI��<�e��3�+���y��^:�o��u���k;^��/J�B�/e����|����(������l��R��{���\1:��6i����@����o?/j����\��B�o�H�r;��l�:[6/G� ��^�M96{��a�
]��������:��������P����.+�*H�nQ)�!'!1dhG	�����^��5�n
����,��S��(\�!�<���q��D�GA�p����������c����;�y��5�;�-2pK�D�o{D���t!�q����
��^��T���;[������p	%����S��{4��������y��i���<\1���m�����h���8+��~l��W�Q�������wk$P��0����b���89Q���4��qNI����z�9�{QE�X~YMv���8g��J��{!x���8�B��d���������t�C$+
Un�����F�\V8
-�De Z�`���N���	c���W�|2!7��1&0_����)�X��"�?[�)�Cs���h=p4r<J�F���|��������:�m=x2���������oL�D����s��{+���I�~H�y���}XZ0�IoU�e�r�6�xKn�~�51G���N��[�v�x������6^���66��:�e�\�4����j*$�1�2��WV�0d����6�cR�is�-Gj������\�`���mz�ewe^�0����1�[I]{W�B�'��Z��5�]#�������,	�1�/F�&2�6������������\2O�	�lI�otW���o$n�mi"K���K�^����V��p4��>������9�Jn,�\�;tW=�r�5Iwm�U]@�����6n����:��V���
�E�B��L}}�{Q�cXf�B�����Y���UP�[��NL���n�+�@"2uV
-�a�Z�b6��+��fI�����DU��Sca�d���;�Q�/��k<b���w�
Y���j�����dA��w<L`G�$���6���8��CJ����O�(���]�������&���Q��T7fD���E����5��j��D����U�[i =Y��z1�m�Jp�������4��v_�5���xB~����9R}
endstream
endobj
146 0 obj
   11432
endobj
144 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
148 0 obj
<< /Type /ObjStm
   /Length 149 0 R
   /N 1
   /First 6
   /Filter /FlateDecode
>>
stream
x�341W0�����
�
endstream
endobj
149 0 obj
   18
endobj
152 0 obj
<< /Length 153 0 R
   /Filter /FlateDecode
>>
stream
x��]K�%����_q6
�\��������Y^�(������3�~�+B�T�2oWw-:++�#�>E(^R��|���z��KN��\R��~z���,������O���/���bDH)�TA�7+��)�`^� �������o|�o��~|����O�����>o�����y���?}���?��E	���Mi'Lt���||���W������W�����~xQ)B��
_>?�����V���
�VD_~x��o�b���E��(|�<.$����/�Ai���p��Q��p��&#w��h�����|���O������tI	k�wn�+'��4.7buJ��J!����F���l��Di|i��&���a��vz�aG�G�^�+LF���{3���C�I*F�H8�l'�i6%/mi�p1��a6�m��������vG�B�B�*�� v|���&��_�7x��,e���Rp�Y �i�c��K7{�>�I �z(�)��gF�^c�Pr�*�E��[��
�R�������g��"Xl�2I��.�����L�-:����� N#r!N}��� N|W���5E������dq�Gc�bd��]d�kv���L�����8��6����H��[�i��	/F2��Z�	�J)�ar�Y?����j�q��"i�C
�u0Y���R
#�\�2����S'�&rI8���$�����Y�;}#�.zt��N��7��)i�3��T����
��)i`�1V�4kt�9��`a-��F� �,�d��6�O�#��Zm��C�te~����0�P>YtgnM�V��H��	�������-��k�
LYe�v��N��4��C}�RO^s{6</��6��(O����[�H�q*�Mc�����(�t��8��H��9�w��z������^�D"��y!�FE�d�gVR$u����;��y��Z����a��/F�I�F�:�,~�!��+\3xP�0������c$w�Dw�CkQ�ipO�:	'$�n���r��lV$�����^�E��kV�4^LL"n!����/L�z1�j����l�t��jBL�D���]��mvaev:�f��!����~6��A��r������:��3#�e��I
��t������^����SlS��2�����	x����6M��.e���+R_H�+�I�7�.�����m0!�M�������I���P���7}={�A�E��E��N<RG>�1�:���;��� �0���#~g��f�S����X��`9w$�T>�3���m0���u�6-�8�F������/��r=a����L�����X�y'�v�:Y$X����_#�`%*���9����=B���}2`'�?��16�m�M7#|ep�e�3uw��)/�2�lFn�G��P@�����7,����m9��}�tG�9b����F]�(���,g�[�o�.�9RO�w�<j:��s���;���:�$��x�&����z-���T�t��b����#�	H��]�F�V^�Gf�r>����(�^��,Y\'W�%���p��M�����PIJ"���i��'��eMA����X��#���l�g�y���v#@��B�U"	("������
����_E�;E�N�!��*�F��w\D.:G#p��I�52����6�$�u��^�V�@�%�f"
��"4�c�Dg��>
�.����_��=n��
�U�i<�M�/�hnT�!p�D�������"�"$�"��X����p��5�\��]2:��
��
���LfN�L���9�Q��m��^�|����M�:Y9\������}���A�����,��������lCsnQd����Mo`����
����K����Q���|�0��s���$��~Y��Hxn���Gd��J�R#�.&�(�X�AW#�
/�� c��$���X��
E�1��$��;��d�G"�6��k#�|�������{�����c��k7��'�����x�pt����XcZhJ��%H�����:���Ios��'ztm��cD���������'���1II�6HW<���+���
�m�]fM��a ����R=&Uo7���]8�odQ^�yF���z������T�(�l�$-
^����c��.t6�5�iP��gG����u
C+����4|S8�Orm,

�@�st	�����A�M��.��N`�Bi"YOg��@>���>���b|�J��/FN��p��mX�-������� ��:��c]�~�<p���������
���N��WF��~^��#�V ���N� ���b�8�������+�^�q�$��4J������������m������/v7=�����s���:o)�M_i7�� t`�\� �(�������xv����qkP~qb�����Rp;q:�o)"v��4Fq�[,k���b�!0����"HxJ�L�[J�ny��7F�hoTk����t�.<���6 nz���������������DVf��<%M�:/�����G�h9�byb���	�
�.��0H�������6yEc�����r:Y�3D�6YET�?�����T23#�\rq������i�-MJ��5��)E�
z��q^(C�D�[�L������6*�K:/���c�Xn��xO2C���qj?_f�u8#�����B2E�:���SD2���t����}@��(��n������H�H��8�Q��6���
xm�4������=)c�l��+���U/'��3%*L�"(*N����K��$~g��@�*a�G��i��ML�M����y���:���O�T#��7v
	r��A
���H;G���
���Mli��P�&%I����%��2�	<jR�
8��^�n��^�X �BG�=J6	��C�.:���A�����C����#]X@�(4hqx�I��>/�5x
�"��N�`~���$�cw�"=��l�B���z�u��B�]C�G	�n�;VZ}����BO8���!)6����]�'�]nCo'��w�&��Db��	X�D>s��,DY�_����V�?�����h��ld�d��=��l2x��a;E''�F;n������
{c;#����#fI�f�J���3�6w>��{X�L�y������L���re�2	�(h �^;�����4�RV�����7F2o��l��P��c�
e��+�p��>M�Q~���e{b�Bg����)������$!%Q�p�����-�T���)�i�1Q6�;{@3*���m�~��N��5FD�� ��G�y����
`�o�Ql<p��u�H ��F�N��N2���"�t]@�3H;i4pSD2�?�3�X����J��v���>~�?���;*�6@��UxE�Q>�
�}���p
s�F�N"�N����Fa�
�QOP�pDOQ�^Kzsx�0;��i�>���[���d�rF��r�)�	�
�3E7"��c�W� ��_��l���\~�$i��(��?�A���h���"'�0������]���u���5Wo����+@�	7�	������o��+�����m&�njo�cI>��3	6��~��IB�(<�0��4|����s�hZ�v�d�p�����8l[�a����E��v�P_���$6�L��v�(0���S�B/���Q{|cfZc|'�F���J���P�����*
���0�Gt/|U�~�@�]2�������-OQ����?�D�w69<�D>
T��X8���w�>�1�wdpm-�I8�b�1��Ld�zt�����]h
�a���p�x��zY�������%����P�N�b'����,0�#����U;��s
;��6�:��s��}�V���7���'1�;1�y`bC)���n:6��L#	�V��/���Bo���R��>�:�wb�!��;����Xm���k��q�,��~O#u��t�qnd�v#�����W��T��+%\�9�(��k��]�"���y��@�R��;u��Hx@��^��o����4�Y���;U����x�~
��k�T6G,�v�8����E�'6����3����:v�����d	3�M�G7
��e�������%&�Lqx.���������F��a:����w��!)�}��E>~��y���|����%����C�g�������y�Sz�y+��������|"����z��E
e����r���JH����T��wA`���$��
w�Pg�����b����l7v@-�pn�.��B�/L�~'C���P��i�7����M#/pg59*g��;;����{�P�3C�Y���Ah5O�v��+����!���+p�F_�W��;���e_#O����|Y9�_��0p���X���-Xa��lw�	[�I�'d��E�/��8"��moc��	Y�l�LF$9������t�������h��m5��?a������&5c��}���9���lI�}n��}6Z?c��}�&�����8]l�� ������U��Js���l(Sd��[���J$5w����TYGl�u��������9*a���vk/EI	��>�[�>''����nm��%aVp���}�W1�9`��}��	c�>�[{��B�I��[�>kWL�	[oq���+x>=�Ik�[�>�$����n����N�������$������c�y���m���Y�$�;�V�I�o�n�G�G�������`�����[�Q���[���f��~k�f�B���vk?N�&M�o��MA�Y�����02o��[�V��nNN=���o���(�����]���<b��-���4/��������y���}��~��~k�����8c��=�y����vk?7�+N��m�������l���X'��3�����pj�vk�MRh=�����1��t~.�|�������wF9
s���*+������c�z�~k�59>1�`���Z#���m���\��fl���� �������BN�j;M�~k�n
B����bs*!���[{����i^�[{l��Q�kXY _>?~����ys(2��-���O�����:�Cx|�������|���_��)���//�	�}������_>>�c�^��
����y}31�g��������_T6��]����������oe����8* 
cU��#�����U!<}2�<f�������h�����T��e���~�hL�������F��L������m��/��7M|j�|�� �WRC�VH��oc~y�.��
���|�k��t�m��
-/�T
�\��x������SI[�:#���>�=0q@�_wN9��3���%�W0�{����3V>�T���Y��3�o��{#S���
R���P�#�C��P����
�|�������-Do�Z��64����i�O��P�2Q=��:)��)��	&���W-1y}�E���V�b�����#�i��)������Q�QK	�GeeO#x�|$�)i$%���TN����)��)�S>���20������Wi�����W�����QL���������?pqq�+�t�#R���]r���B�6y�p~�buz�$�8=M4�����8�1?���.'�h���PF�+�<����������q��+	��a��"�t�:��2��m�q�z�j����qY�VU#a����NG��X� 5�jLYT�2�UM��Qi&5�	*�&�~��$��������q�Q����5e�l4&���i|�L�l,*�V6Qh���O�"�����Eec��Z�H!SW����i����JU`y|�S�Xd�U]�L�56���7�i+���&���7��P����9�n13���������js�8�Y�<C��2����R:s��������O�����_�������S'e�6�j�(m�XK�sA�����i-m�	�5g�?��	����)St_�6���be$�-��{?8K����M��3����$
Z6y�=	�<��KX����h�+e���et���1����JZ
�m�\��������d�������4����2�>����F�"Q+++tZ�}1�M�.c��yB�{�dUaL5���A���k��n��������f��f�"�T5����������x�D�[;>Z�$P�mgw�,�}����U���\D��w���������Z\)y�n\7��n�};�4��/Q�S_b�gS�N�$b����ga49�%�}�Qnpy9�}��>��O��h�h;'��M��hk~9��o��������p'�)Y[�!��l`E�R-�ZX��4��&kE�_k^���(�f��Ur���6T�u���D ���y�D�����E��:NKc>���V}C0��+c^��D�_0����b��_��?
��"�QH�h���������rt��r"���I�2��Iob����Z3�����F�jI�����Q�lV���P�=�Bz��Y/���I���X�R����P(pu�M�D83��Jz��5*Q�v���<`DP��g���k���9��A����*9���$�����@Y�l�p�&�32�	����ju!��n�5�'�����wf�iuS�����F�T���IV��B��,L�L���s�����F�����U5\��p�D�J��b�Ob��3����VHb�����cL���(
e��I;4~�i��'��,k���[�6��b��@}��C`��cH��^r�b(5V�2�����yp��~��)�h�-����q����[4/p\h���x���A�e����JEX������eY����u��U���F�	g�2�R�>�T
B)����Ba��0+�����]vHwa����.�E�!�Trd����q@����i�e��A��r��;���(���=��^���
���yII4�`]"��I$o�P��6e�r^���Q��	�Sa��9��|��5,*�Em�����.�%�)a��0���Ka0J?�)1�&4���K�K��=�L��W"v7OcPU�|{�X�������8��,��]��|�a~��~�H[�J
�=1E���j�e�3��j@����hl�3���I��
���[G�BM,��
9W�<6��+�f,QN��4��Ri�b*���eClN�&+��c;�hzH���^[#�f�]���8/3Z�(w~�P�%u�QTx������:*k0$�:�9*������U%��W?��jp
MYMup>Y��c��Ch���O	MQ����c N��AUk�8��>��Q�b;�3�*>�������i#�%U\J��!�nDXZ(��=�X��U\�X�=����0�X�O��R9�[�q��c@B@1b��������'=�f��Z4��`�*[�1�B2�e�`4�>��B�m�57�����|���>���dG�%�:u�xt�(�y���M�jmH�Cg�dK���i~32�U�+�`��F���\
m�H-E����D�T��8�I�X!��-����lI��%=� 
@���L��s��bJ��4.,M�cO�.��>T������-i�-�eK�7��@�UX5�R1&M��]��,a�v�Gs�B�-5�S�c���TF����?�������O?��)�,l��\��I�<���(���D��
��GL�@�����a����TR�ZU�uz��<>�
���*-aYdB-�z|��Z&BR���S���`p=t]����MK�WB#�*���f(�����UW�7Ba��fW9��//sWiK���X������D)�
�&HC���Gt����^�y���Z�Q��<�I��^�S+� ���(�K��7JK��"�@��ce_���1�?�����C0!4*��'o�8
��E�#�T�1�g�����(Bz-7s�)��D��"�B�>/=�\ F�cjYN�V"�Lu
G]iTU�uq�;8'5���LY����V���7�Y�+����n�0*jw2�nG=p�L����LdE������E�`n�Nm^_�E���Z�#W�(�!�ZA�V!LT�upk���:��
�2��e(�S2�eH�
~V
Y�Zyeb�~X�bH�|:��;2����e+2xA����h+2X_d�X��*�V����:]�~�e����V>�)�����d1W�����������yC���?`MpO��d�����������}J��K�
���@c�I��OJ9b%��P!A��F�����0���idM5����z?����V�~YTxT���.J������Z��#>u�G�DUx\2��a�v'�m.�;d�H����$^���^�����8���S�x�*���/C,�������>��TM��vN����Ay���FG+�>�
��A�	�=��-1���PD�n�1C���F��-�R���p����X}^��,��w������REgpK�$����������/I�q�R��^.Z9{�� �a��"G������Ut^���������w�5$�%����PF>�71�������
=N�p����y���{bpgK�BhB>����G	S�Ch|]�Y���������t�A�����B�s�d��A�`�:_��*j�jq�j����;�U�����XNE���K���l=!���*��_/~��n����Rh�B����(�����9��l	��n���+��N��TJ_�*^�G���W���"p��,5�1Dhz�����Z������5��qD6��b������l
bg6���E�qa��*p0�K�!J��R�v��(����#�_���a�6���@m\��|0���`��!�q%���`�&�O�Fq���S�{�
�'�0��"Je�
��N1�Y�$m���!�0��nlk_���*^n������Y���"���6��-+���y|��d~����j�m��qk��I��j���5n�#��m�k�?s��L�ZR������8�l��q�z�E�[wt�[_�HCM����`�F�}�`H���3��oc]O2q�jH��c��J�v9�O��>�k�D����5$�J
�v�#��M��n�K<��0/�Ye==����D�
��n�o.4�:���4�y��������$)b)B�����JVW��+�-o�sRR
�_�����Veu~f�BX��������a����cU�Mj6KE��l�������p���8g����V��:��I���G�_r��������������_��KT�6�����eKMy��C@��2����v�k�]����oxE����5:U�

��_�XE�_�P�x%8%�[Z	��kC����c�v8n����n7q�4D�T}��8��:��p�D���#Z��$���+J�Z03������G�b�7#Y*6�PRX!%�8��@J�"���W������T�z(���!�������
����1_����b[%.��������C����6��P]c��a�,,}V�>��\�u<���0CrU��R�����df��fH���BT�<jf�����>��
b���T-8m�>K�y��y�M��5�5����5kd��w �c\��9l�tS�S����k�J
Y�� �XO�1���k����AkM�d��H>��`�.t���W���� ����tM����{7Du����k��U�U���5m0HR�m�
�1��g��L^�^�I���_f��Dp�1/~v%];S8�E���6����fjawV�:���j5Il�|����T�GI�6��6�#�=�(���Y��ppU'�*R%rpD���U������Tm�����?<����|X):�?�p��;��������||��}�f>�R*��'"r������E������+so[�7r�#A�'�4�9�{q �`@������b2'�d1��#��!��nA�����/;�N�
F���������\:���3m��>;���(�Cm[�������
R���4hLKY�u��Z��B�*������\`k�����\1���U�%�{.���<�c�c����Q8'��c����O�8�����O������.o��y��2��\:JOZ�AS��]���/�v�.���@
�F��Q c)j������[q�Y��	m��V��!�����V����4u�G}^���KB����$� �>�>�
:�z>Ik{��_��4�W;B���"���Y�g�Z�����p��:�Z���:���=��t������7!���v�~�J��@�|@�R7��sq�R3�i���H��2��C�l^%��T�.<+4c�	%�8��=�5a�;��&������&���{%�rmj�6�e�D�(C�m�y�?
�/�x>�/s���<���2>X3�d��X�3���
����dVXf�5��8����x����o�x��/�l<���;M}M�t�8�E;�c��Z`���a-�DV���(���J�i-0#'�.��S�j��KO[
�������4���@��A����P�O��Y��&����\�����W�W�����\�v�I$��\V1�
>p:��<�_�������d"	yW'G�j���N���7t�R�$>��]5Z�n[
��3^F�s
v</�&� �A86����n{�e8yL�Z�l�p=���!U8�� ��NJ__�:�J�WdX�����nq�������`6�D���Y���Dl���B�i*�v����yW������s����!W�28��U��/(���A
S��dL	�#;����}�P\��h��\U�I�b��87B���\��-��KA�s�����V��p��bb�Q���kQs�����=
<�A����^�VuG[�0i1�'z�M�����5j��p�"CK�L-%������pz}����A���g����u����0���������4�itk��TC���u�����ol�NYV���Q{P'��q3V!Z��v�gm�A�%m���B�}�`7Qw:R�Q#������ZYj�a��H�'�-�.�
�6*#�,.v`y\q��9G'���	7��]�)�N*.`?5�|E�(*����y@��,�I���HCfC�����H��G�}WMv�����t����S9<�(G���0{��g/M����|��+���4i	���bd��������x�w�^������<� QP1���g^��2i=���k���irc8�.?�sp���1�w�����*���<�k}[���u�8�~P~����	4���n�*{?K�����=~�n�����r-�tR;������g�\Q�}N���t�`��g���X�L�|<�0�u�x���_�;&[�����%�k���n��.�lc���>�5���~�T�"3�qo����K����P���x��HE���/^b�w�w����.n��xg�\������1?v�v�����A�����
��,����<�Eg	�tv�k<Lo
����_�X���)|8����*���3_�CQ~�~N|�<��z�A[���mi�p�QD�I�����:�/�����:j���;��
Z��8���=	��o
1����WT�������WH}U�����o������%�}}��%C�#����Yb ���{6��H�������m;�U�DZs!���8a+�
endstream
endobj
153 0 obj
   11541
endobj
151 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
155 0 obj
<< /Type /ObjStm
   /Length 156 0 R
   /N 1
   /First 6
   /Filter /FlateDecode
>>
stream
x�345Q0�����	��
endstream
endobj
156 0 obj
   18
endobj
159 0 obj
<< /Length 160 0 R
   /Filter /FlateDecode
>>
stream
x��]I�-9R��_q6-��t��cx�����jWbQ��.@tEK�����}��s��ZT�+�k;&��<������~�����'x���o��M����/���I?~��o��7�bJ�����F�)��A���v�������?��~����������������������>����������
�6���+����o���������/�{��>cy�����7�����/�k����8e��)������������!�(������_y�*z'��?�M���3/�Z����������
<8e�����l~0�&,pDPX�V��J��6�1�6!�#���x��g�QY-,�L��v8f�%�M�3�Sf���d���0s��e�h�,h��������DgR*��1.�&�:��d�����&������	�^(��K��������������F{/eIw}�x)�^��}��<�A�����&�cJ	q(�Vh��vf�y��I����u�����=�&)��#��3o8\�1x_�%i1��"~�W	U����m0�W3���PV�;�H�
��i�3�&��P���
%:�5�W%�dq�>-�U�D4O��Ty���7�iu��J�c��37�����w`�<���*�Ob����;��������Dd(�e�/rM������	~b��$�,KV!L{<�d��"�e,N+�`<&���0�Q@���L��(��1��-w.���s"O�9�:Qe�0��MGZf.���c�$��K�U���bK&�l?1e�TK�i(-�U�@[�-����x�����_�tX#�Q��]%���m�t�-��:�����ig7�7������u�p��o� L���q=�Dxu�g������J�W�������@g	Xe�'Yg��cO$������F������Dg
�K��1o��x5��N��!���P��{K����s��w��^x'-�?/��5n|Tb��h3�7�O���F6�cT
����W:���S��lQ��<�Fb:e���%6f��X��x�?y��������@�+�R�����{�l���8�J6h�
�F��7(����'�+��
r�i�����vJ����L�|5����(gQ��5��&�la�?�����J���Q6�a|is"PR���
S6z�@��R�y��tx���vy����D"��T^��^e�&a��x��E���3�$���*qA�7O$/����T��I�W�"�Q�|g���kdI'��s��/'��?��@H�U���.�%qoRW�W�ya�Lb\�w0h����)yb>y0O��<I��/	�������-G�;_�1��^}�w7��Z�)��z���"��+�?�u��Wq����oW<��
��8P}��w]y`&���i/���/������	��>�XJI���x���'���;�XN�_�v���L���0�&;�E�h�M�m�1]�d��0wac���������&�����R���<��I����r,����Y�\�P3Ihg���0���&�
��w<<fFW����	 �F{i<���p'�������2a_6��$;��[���!�4��K"��*N,(�a����x)[J{�mfN��)!O�G��;\�~��q��?����C���U<���8$�$��o'%q&�j�]Z�I!`0~<,\�K�U����)��|3n�t�����pRZ&���*lS5r��������I3b�4��#�G�E?�_�I�uK��0o����*�Cw)�,��)b�"�E�����9����6	����(�0���=�����W�W�gy���V1��������y��$R���
Q�a����)���7I�)m���y]����p���N�2b�4�Ac��h'�{��Lc��'�M��������3sY<W����&�c����Juv�k�kB�������A D��3���Ct3�6����c�g�L�Y��&J�;q��w9�+�TG�y>+��T	��
s���}N���6\y������������R/K{��D�h�H>
IG'U�����>m���
L������<<[����|���|{���0�c;)A�|�y^}�]K.1X��o��`T���Ou���9��%���R~�"��9	���K�Zv)r���]��Z&���2qJ4��;�b,�������mRkL�j�E�r����M��e��s��c�0f��+�[23Bfz��9��H�4dX
�g�<�������<�
$O�W�%�K���J��X9D������5��|�?<���5���I�-^��Jl�	��X^�'l���������" _�X����aA,=(���
�by���<��<�S��fJ�h��'n���3#g�{����=�\/�>(����������<�zb�0���@���7�V|=H��p�qz������7
.�&l�
=om��Jx���q~s����>f�����yEk�(o\p���)c����EtzU��^W~'/ ��/u�c�?c�Y`N�$��X�V>���Ew	�*KsVo���c\�A���M��n�9���x5�R@���#O	��W�sw�Mo,�)����)��K��=�;C��az�kR}k,�N�z�����x���$�=5�^�_N��'�6e�;�����z��K��#e�~UB�S�lH�5�1�O�)���jS��"w�ig������P����+����?����k������vo�P}�<���x��<	uL�����Va`��W��X���X�]�'��T�EgQEx#��u��di�J�k��:�#��"x���6u��,������;�����7��sd���t���{R��iM��=<{��D��<?g�I��I��C�zI�K�[%<�y=b.�j�����|��d/-���I��2�K�yb��tzI���8��aa��#����3�L����'��������l�L�j������2~' 8lQ��<��a��e.E���a	��@�6���)a1�]��t���	?��C$^�
��S��D�x
���g\c�|a	�jg	��l�;S��Eg�8����<Z��y����������LLa��{�g�g`�$78��0�"����~�3�@yd�j��EL��/G��}b��rCZ���w�S�^$��o��a��A9�����D��e���.��^�W��7����O��H2��wo@Y�F��'����@�?��uW�09���;����R9�o���_c�m�������u�����
����~h�����w��z������Rz|����}��P�6���z�C.����|��
�5N���
��^��&�`�e���mf��#Gh���	�N@�Mi�H9BM(�G���q�[Bf���`��}$O����zP�3�
��+����#'��FE�&(���.$�b��4�B'��iBT��Wh�����-J���_��7��Z��B#'b���� ��N�hg�k#G�MV%mfd9"��M]�3�IZ]��������Z�b�
��F���.�:t����[�u�����6t���.�IP���;T�������&�����#6�$�����X����6t�_�����X�T��4���4����F��mC�y�SM�C7�I�u�u��V%��P��E���:t�F��[w���S������B�O�
��ET�5��36Y����}�6��*B9"��+\v�����7{��+�mF�?����>t^3D���l:c�)���mC7�� l:c-����>tT\����<rAy���
���P���mC7�A���
��������X_��[����=ji�o:���|X�u��f�����6t����~����2f�o���J���6t�:��e�}��#��Z��6r�R��gh:c�Sf��>t�t��%��2r���Pt���XT�����3�Y���6t���z���X����V���\�tZ�����F�p��>tU������p9���n��_:��QY\�����d�q�~��
l,�W�`���k(�/��Cg,@�]�����S���Il:���li������Y�7<G�.�_y�_UE
�����2&*�S>s*�!�x�UU����������������{�_R�����\��������|*��������?,">��Y��IA�V��������.�=o�M��l��'h���QyOKHn ��|�|n�7�7�o��������:���������d��������T�����)�B0����|o8�������1�g����'��L�	��!������������D;��A�@D��`�\��v�� I��"�3`�`��������{��s����M|�6��&9M�Atm��z�b��B'4"��3U�:���J���[I���9
x��'����4$:�Sg�$b;�K4(L��Yn�2U�C��$���D:��!����
�u�H�u)��_e:�&�y�a/���(��2x��*��?MBi!�N��Cg;O|E7q�Y�F������o&r��g�	+��3�����,=�Q�Ay� �
}������F7��i&^�����)�r>���6h����hh�K������;a��A$�&��2l�Z-�%*7z�A���i[y��#s�b���,��5Ra���L����9aB�������[�\�T5�)fvw,��iF�����M��]H���]T�*�i��`Q��qd�����*�A���dn�����M>2��,a
�B|&�,K6����bM�� Bv����oF���D^�(m8_��(fJ�!����c����Fi�Ng�-{����X�y�Y;}�V&1vM�#�_�a����B�nG*����y�N�����Q���$Aw'/��gIR�$�g��	��X��Q���t�Q1������%�:�5�yB���^'�W&���G����%�Rjw�+���3{���0%aoJr�2���.�s�7%C'[����A�DT#�K���!���n	*���IFa�N��`���v$o��{��0���?�{a|o������yM��������Y������$7�"�7����MC�`���,�I�Jd]�(�<G���#O{�S����9�lb>�����b��_�GczB�4�
^|�a������J9����CM��`��^$���"����O�1���9E"���
������w���I�	��t{����
I����ro� �9���d��+/��l�@�������;��i|��+����M_CI��14f+�\�q�n���o����&3/���0��R~��	"�x��q.�0^�M�59�?,4�������>49�%kr=x=�.7�4�o�jN��!!kB� W�E��:�u��U��9�8@����'�^��
-s{-�->p��e��QS�+����j]�m�������r7z�3�IF���e�o��K�wQ�)Jo����.�^}�����xJFf��[�ri�. �r1K�Y�@���pi�G~.{���n���.J
]mf��fi�ri��`�
}�zN?���P�����
^h�]~!��Y�|�}����R'�4Y�2���i����t�d�6���=���6��%_����$�����=\@"����	y5���SO��)�4-����[]=]�A�t��s�h:t�~E�A�z+C�G��8btg��6'k�lu'�|��s�
_8*�����V|������<�F����T1��}�*����S!�]��T���.@���
@�@����F���:�=jH�s��hZ�UC�
��1q>Y���d����%�g�6�y=�,�%�[���h.|yq��Cc$��	k��k�y����O���"9��O]|�����c�����9����q�N��� J��Y�,�$V��;K���#��/Y��b���P��q���Os{����-��k[�.uJ9����B�k����"�y������_'�r�8�6m����x�)d�h����FH�C��Jd���������r�����C�)��^p����r(1-�F7��~���X�����_���XS�-{�^L��:
�D�}�;�l=�^���2nu2��aP���V���,a�������j+����/VHl�����R!���[���/t�]�j(��8M8���[](�:��t�A\���
��tnx7I�^|1�����C������D�\����F���(��#j����Y��ln!-���
��a�S�<fW%cKh��y�����Mf���L��vs���{}�+g��\�x
�m^������U3�iY�bZ��AK1p;k.q���k��ew�,�V�y�;�R%�s9G�5�tHMU�q���z�$�J`q9�r�`�e��� ��2��_���l������$q�����$�������P��������Z��Ms�sN���}Bm44�kB��L�Sm^s�7�7G?_�!:{���I��gC�[r������{]3���������Zhu��ZO�\{�����9Yn;��)����	;�,S��Z�VQ5�v�40��kHf�����Sm��X|����b���y�������J:�7
,.���I���sC8�a��e�����oC3�5�Hb�+�����5���9���9��UI��9&M?Y>M���Ve_��2y�)�?U���|��b�F��b��;��^V3zY����$]]H_�n`��=�)�\�T�(2��QN�p,����'�vJ����S��$��p`����;�>��-td"m���d�o�yE�F��
����#�����e�������5!��%��@��S�x���:����N	����ZW��axnum%�g���gs�lcI��bc�q��~��1bF�����1�B��1�c?FR��l`�t��.ZG��t�Db���Cm����g=F�a�~>�8\n�./�`���x>F�������)�
�A���T�V���}D�n�^2��Do��?���K����#�����U�����Yv�!Y�47��Ibo(���9|b�K������
(SIv�����2����`���r�����lE�8�����(�;�-O
r����tG ��<O��&c��=����Q*OQ-MDSX����������Z���0j��n?8|Zp�6���u��T�G��R���:�K8�39�S��(���_��l�����#c% wW�V���;�\C�e)�K/#��)�s��R���#	�L�J	��d�eD.K�#�@��tNR/�~����8�t��!M��;��s�F�������}�i�U~M@��O��%<_zf�:L1�����s�!��z���y��`�m=e�R�O���h3�G>�Ok��^S�����9"F?�#F�S��6��%T���`a�K�����z;!
Jo2�"-c�pt*@�W>l����;�Pa	�>�6F��C1���U�je�_�0�	��tmm������4��z
�m �}�+��v&_M:��/7&���R
�U3����A��h���k��H�(1�5�"����_:�hW����fm�?S��{�O�1�v�)����}dB)V#���:"1�vo|AdqD���}k��yB�1�v��^1b���5�����i�����G����!2�%�����'�X�4S����xc��;�N������%���1�/^_5��t����
���M_r�cbc����j(��{�Jm��]�B�_T�����r����:j��I���&�v"�\����1+��[������2����i�z
&TgF����3�Y��Y�/�^q��1r����<�
��&��b���?iB+*�Fy�8i���k}���t\�]e,�/�2Z.0w�=�0G�g�/��")s�c�F���=�v���h��z�����~��B�.�0.�����k��Vi����y���(�����|�7���9q4B-~��;�����u���lG,&�?��)��9n��OO�������G�^�o�+}3�O��
�Qf����F�b�����������R�p3�t���k��
��T�o��W���dhum.(�0RO}E�J�0���MH������1s���������j������(�5:�0��#�j���
C�q_DO��X�i��P�D���=��=mJ��}��;���CG��@�4Y�����c�v�������6�7/��S7G��2�?E�&�!�^]��:�����P�]^��4[3�������"�&q��g���
��Tk�|���'!!q��y[w��T�bwj�,%e]t�2�{�C���	��%V������+V����7#�=S7������k���e���a�����2�2����j���z���������5���+<���������tc���/wa���]dJ���������;�����'#�ao]Y�stb����K*5����}����%�M���R����1���Z�(M��R��	~S�2������>�����������s9��H����d�b���������nt��������z_�|��s�&���a�~���@�x�+]�5�^����K�������7��#�(��z��F�8�5�j�#�
�
�-���?��[��^���_���V���e��.����o�����O���'�?�S"f@4#Y>���'f�Z;�ZuO�f>	��3_��PZ����:���c_C�e���a�U�{t������bw?�P
����k��g�5q�����6gG���,ftH3zG��K�mC��L�\�R�cJ��`�'���
vUV��8E�wn3n��UTq�i��wu��]9�9N����!V9�(����:1�)��&`����L��3��%��$�1���H�x���zj��#��������^��4|���5����:��l��MJO���fRO�aS�aCj�����N?�O��F�&�,����0(��xG���v��@l����_=y=Y
������e��,o�d���PV�����,tNI)h��q|���8��\��1�f�D�����a4�%��	�����:������R_D��������6p];Lq�j������az��JV���s`4���[g��������F�����c����w+_u�MA����x�z��#���VN�x�+���4�{����L)
��,&���Y��������������Dh
endstream
endobj
160 0 obj
   9264
endobj
158 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
162 0 obj
<< /Type /ObjStm
   /Length 163 0 R
   /N 1
   /First 6
   /Filter /FlateDecode
>>
stream
x�343T0�����	��
endstream
endobj
163 0 obj
   18
endobj
166 0 obj
<< /Length 167 0 R
   /Filter /FlateDecode
>>
stream
x��]��u�q?�~	��pdit
�B�����������i�@����6��4���]���e���fF��i�z��|���|�hxk�z|�����d�_?����k����o������1�X@�_Fb��<�^ji����o_|�.��C=>�����?~��?>��������������_���W�����xW`������/����'��������3�a����R��@�����PZ?������0"8��������&Et��
bL?\pJgtT�E�U>����[���?i��B:`�g!�e�i�	�����og�w@�y�!�e�����^A"��3����-�zg��� ������^!<��cF�����&�Q��~>�t�1���]�G�DqK��;�8��~q�����G"��.h7qh���+ ���2�X����S������'�W�AFIu"��`F�0�^xC�l�aV��e�H��"��F����0rhD�O�u�"���(��b�1r��I\�?F���+k��7B�Q����C���F'M�?F^
���7k���x�P�U�x���)?^1�����N(��cW�qp�u�6����xB�F":���X��>�q4��8jd��Q���`ce0�8���o��9~��'����9��V������ �/{������������r�6����3�	p���?�<�i39����w��CGq��92�8Gg
;G{��*<���U~[��b�����S������":�Q����hd�1�w�nB>���O���nFN�����O<l5��A#����K���/�����m��7�����c�=-.������Wo`H�H����J{��S���$�G�;>����?)���L� \x�l�q7)/D�MnZp�9��|d�m�b����l~ZU�����w��R�V��cd'�^�vB�:���Or���#wI<��{��o�V��G�3��M��q��=����U������/�(e|�1���f�H��Y�*/{Uki�������v-;1��=1����������<��U!@�}�������������������z;n����,&�w������'fv,�����{��o\-���#��cd]=��vN������}�����A�/2Wi����k�Sn|ML��k��m�IqSGL���������w4O����<�'a����������F�������2��6�yL�UOL���]��*7��3&)������A��u�$�nP�����"SI����nk�rx&�����wv.O��fC���k�O��:S�PJ�U�=�������7����N���"?�7>G�����6{�����x����e�lA�������&BF�@)���J.������y�2�[����u�,Qg��[�[�Id�<mS~�5a���.����Do�����/G�:h�2�X��)�c���9g�B<����u���2[,�;���4>�7�G~��g:?�AXO�$��(RN����8����r��W���a����M�-�"���9~��q���N#�����c�q�^9G=��s~�{K]�G�G��������� "q\`�nUS��4�+�@��
O���i�������:[������ �R�i���jS�N�FXj$��Y�� 8��=�Q�c�"�icP3u^��!�NnjD'35C;��~��?�y<p
�u�8c'�<��X�0&>��k��r/�& c0��&��`��x��g��!����A����?��{�L��%G ��2�d���T������������������3������v��G����������M����):�P��VGu�����M���F�w9;8|I�LqJ��@N�6����8�\��IZ�;��x�	9yB�hT�G4��#�r�B�����`�G0���	F�@���dl��:;A���MO��FP-!�{]9�����F��*h�L���_���=I����K���{��VPC;+���p�����
��sd�Kz.�����h3����G4b�#9�l�,���i�^/����Fbz���H��xN&�~����#����.�rc�'�P�'�2f����#�d����'��������:?�9�9�O��@H�B�WN����n4����#����� i��Af��B� �F����sp��Ntr.d�V"2�KG�8��0'�"3����^5#���E�C�����YB
Z;�%�b;���lI��9:���7��F:/@;3�'i��A���F	�^��5��@:����!����xw�1�_#i�����'����Nn�|����N����l�FP�)�m0�6Y2){��I{�%�g��uV2�;J�c�w�������1�����kv���9E��W����
�~q��5Nq��q2G�Im��X���t�x��U� ��?z���$�vV�o�r�UtvC	��@L�.?�o  �u��C����Fgr��X�9H��r���P�-%�Z����k�z���#y<WB�4g�7��4��Y�L�)���3��F��@�'���}�'D7�X�I _�����{UR�Lw���+�*��DE������O������<������a��{f�>�[��7�?��#����w����6�zmY��g9�C�����F�0��3E�s��G6q�3��4�OZ�'8���^h��9�������������Y�M�JN���u������GQ
e��?4�H��<3��U�T7��9<w�9��*]��������1�#!R��y���F����u�����0o':[<��2��b�3\H����E]4~Cvf���X<c~ �������PH�L��N#�������G(�nf����>�S��@��A�#��Q"6����Z�Ej��r8�"p�Gz����Tw�x�	Y�������?�����(#���mxw1����j��K���;��p�����t���QQ@ U������R�s���]�Mu�D������o����v3/Y�w0&�i�q����{�M0B�`��^���s�;I��Nr(2��c����@��C���a�EYQHI�����f6�,p�o��4p!?5�������A��\Z�O<b���2F�I+r/y�k��E���p	c#��6���%��4�1Z���$~�&3vu��Cnro�����R�m��#���I�Q�Y�K~i��n@2�]P��_R.�T��,� K#�x���x`�s1F�I�X�<p���������\�"���s'q�'�gR�����,�C����cz� �&��H+��m��-����X��%��KRSL�&�V���u7��t\9�%D������5e���8t_�p�3��� �%p�l�,7���]�wr�Mc����Lg�Qa<�O��'&���8-���qh|"G K�+K��[
�� ��n@�&��`�����i��^�@������3�(��)&x/\���h9f�WS���.?���|F�i&�������.�@H�.�@�c������d�9;�k��g~�k`|������o��ou�k���,,rJ~��/ ��t�"
���q�;������c�]���,�6:��I�y-]fk#e_��o�%�|_��Yf��A����X��7fj����:���x:@�I�$z!YW��9<����&�s`��r�����	������Y��K�p���m%gg ��;��J�1����$�L8�����{�g�VW~���������:�� {���Wg
�Q���o�o$z1���l������;���?�Z����0���"�J�73�K���[����u��������<��&�S�?�}��C
���M���=��P���2}��:�b|�;#�
�����p���>��R$A&�����o����7���o�@%mz�l�@����l|r�Z�"�#�=9B}�k�Y�� !���H|r��I�u�w �����IQ�q�A��=I�&UCB�*�����V�#�=9Au������5.����'3t�����8�|r�+�^H�$e�_�'/��;����|r"�m(�]��H�n���(���'d}rD�O��I��nO{��QH�'dyr�3j%�����L�<uj��=���6
���<:�����M����-R�l��GG�OF9�[��� �����
���-��X�LU�p[;��y����O�s��Q�y���
��2a�����Q�K��������cyr^�W�����
�f������f~���L��H=c����Q	i����n`��j~�������?�����s�7��`����A�|������fe���_{t�lvLFl}t����?�[���F�0i�����:
���-����(���\qVtj_�������\���AL��<8���O��=:bsC�0�����U^h7�r{t����[���	M�����X�������Q�S�$\#�>:���pv:u��36J0�[����������*�������`��v{t���N2���F��y���nN�,��-���Z-���\��:������r":��[��u^D;�o}t�zAM:�=:������<�G�y�:�k���������L����lyt�7�+����=��u"�{�=:�s�VY=�E��y^PB�[���x���b9be���������R��M� }Y8���X��|��������|������|�O�6�����>��-�����y����}�T����n���(�O?Mx&�eH!��T\RR~z�!��P�A�X&"}�<�6��  
�
j�����B����A8[B�Z�����0g[����w�C|F�����z*e~���JJ����3��������w������RORobn��A�N'x��Dj;s���#����NEp+�����vaa���Wj�5���bU���hb#6H����ef&v%��?��k�.�j9Q��jw��Ej;ER�X$��J����wTO����'�>����Fi�W}�d]��3��Do�F:�4�M��zZ^�$�U�)z���7X��%7X�?����SY��n�B:���dE��i�z�O`"�bE)�.�1 �KJ�7,"�F�m��I�y���N����
��F�H�^���?��xR���3�CW!���:
��
!I6(l+j�"��;�t�A,*�������M����D:�D:5�s&���J�o�����}�)yv����@��@�/�9�9+!d����t�������8��G�-RH�mE�3�Z�����qIpE����p�"W����]U���z9(���p����*�?����.x:oB�z!5�r�<k�L�N
!����4�d!)��dV���PJ�h�m��R�`+L�&@a
�)L]$�)�k��0���i6Y�sj���~���4���������xo�[������W��w=����?��png�"��s�� ��&��C��Q�|p����n&��`��L���]���S��9��NP����s���M��m��R��������U��:YA�d^���&s����7����FC��
�����q����P�n�b*E9Z�L�h�v_m6O�u���pg��.O�5;c�a�����/��Y��~��5��~}?��<�_��rG�_W��g���O�����_>�|�(����U�`/����It1�0R��"�4:��z������!p(�`�(T���B�H�T��=x��O�����3��v������c?.��c�z��pN���v���V�E�g��_5��x���������C�����!j[����a�����Iu�(�^�����m�n[�

'�����T�M���hM��	�����Q�ph�-yzu�D��t�P�V	^���V85lU}�&�Fk�O������B�*mTwE,s�������}Tz SzEw�)�l����]�t=��V3�����O��};��^�(��%����!��IBW'	��E���G������$q����hP�d7S���F�������6����K�6���	�S��8��G�]`H�65���G��r�m������`J�,qN��W�O��l����Hu[_����aY�)��t�������n�Ky���=n��X^��P���a'M���.4
�>�N�sM�N.T@�Q9<kt���N���&]Uv��^�^U�����U*2r���Uc{�\����W�fn*Y2WiF<:��/�������(���i �����Cv>L��H9"�ED�0��*�\]��\�F�V��j[-����K�Wv�j��0x��g;$q������Q6�#t@�9�12����Fz�D(�1�I�%,n�"�y�O���I>l`y�����U�����u%R_vI�{z��f����a6v��Bh���p/�@],,�K��ZX�YX-+�XX�TW�la��\O��_�1kF�i
����B�'���]���kJ����)�~��:L��W���������mG+ $[4� ��k#��Hs_2��,���YV8����f���k�/�oE����+��~���q�8��cj:L�'�B�V���n�U�����J�e��b�������=�!$5�1u4{ Ex�����J8���i~@g�����Y��K�	���R�S�.���M����Qr�t��tA,u�fg�8����wr�u9��Q��F�S������t��4F��p]n�Y�xb}�+g��TMK%��*V?��-���8�~2k��	k���&�T����������O�N�u,��Bo�2�����i�K
t����(���*�H���$?�M����`E��q����HzI�]�-���IC�6k?|�O:���Ko�Z>���n�����6��C��q������l����u�Y1F��?7C��j�R�n�]Ll������\���Y:�As�u��
����t�!����*�6(��6EL�xj���\�1���}~���ooJ�X�-�~�t���?���/�����FWg*-�T.X�����T)�|���o���������}��7_��0�M���K��#'����B�v�Q(�;B=r�E��MT����������1	)4��sX!�~J�`��M�F��F��F0avw{b�l��hZ�s!��H���K�Yt����\��R��P�Rk��0�r�Q�|���Nm*����{��=�Rl��u$�k?U�R�Q0�d-�B���cd�x�����`�oB�}��*�c�&4��/�E���:�B�B��V�A�>3��T�01I�!dW���y���t��n���_���I=�t�Fc�	�VJ��Uy���T�ZC�U�{�j�n����36>�P=2 �^O�b^�Ed�w�2WG3�Z�t)����4�����,���a* K�
��S��>)��Z�S[��r�C�u�Nu9��NN_[@�������`9^�;���)�lG���!b��&{��0H!�^������U��x+�).8l�lq�,����5�>���I������p<�6RqU����h�,n��2�&��a����
�DA��i9�Z�s��KJ{[A-���Zac�6���9HUo)�P�Ag0-����R�tI�V����9[Ke�8�
��@fg�Y�T�fgK�M�
=�8	�:�s�z��M�|V!r��%Dw
���Z����$bR��&�`�Al����G!�U��%�w%���6�u�k�����-n�N�@��8��B�V$P�6n���I���s�K�^*xJ��I�tO�es|/����q����Ix�<~�!�C�-���hL5A�CR�f�/1�`]����qhXd�%��k���3��%~�zU���&c}��'����o���x#��J��U B�.�r���q�%�W�M�����XM�w(�:\���b��:2k��Cg�k�$���5I���Ng�������C������
�S�5��[R[W\\G#�$5����I�zS��\��8,S�s���y��3&���{��QW*�*�p4�.1rX���2�k����*�oU��R&���&}�~��R�R,IY��B]����OUb�Z�X*�%�)_��qM����0+���"���T��}{������`�5��|�K�z��	���Q�`5(���'0!�fgE���?\�%/,��b���VMh�W�Ii|.�jB%��m�xoM	'��-�mb�W�ffS������!�qW����H�G��c&s*E��[2=�E���z��+?D���R��!
PrH���Z����Of�R��H4���
���Z��.��hY��R�>:��I��&���P{���k�C�vUaa��9����'ez�P����=
�����k����Tnw��H�<gX���X"K6>m�FJ����!_��5"
���i��3%|_C���PBS���L]�
r����������7u�@uNP�{��/��-)��W2�R����������(rVz=�l!��_�I�\��:g��.a����������WH�.G���^�K�B
��4��X��V��f�Wo�����Nl�JS[�������R�ID
��yLS�D�Z��R���D�Dj��Z9�N���L�8���Iq�m�j�"g/{JM
��������a�mEUg=3.�,��$ �8�/���!`H(�X���|J<S!�����c��j�}W��y�0���?n����|Y�&iyH�o
��|/��t�%���H��H�S|7U��&�l�^!��*������rhHR�V!!u����%��4�:hS��~�N����c-0]���}��e��6���L�`�l5����NW����Ej�1��I�����;�K���v����
�:�V���K��eo�-����N������U.Is�6%����qg��8a������X�L��	y�K7�Q���)t^�Zq69������6�R7�+s�17/$�t~�N��k��r0�Z�RN���S�`[�xg��xUt�,����SU[�W���L�"u�W=*���5y��U���n-Du�c[�V�E��R@�TI.a\��tq���eAKD9`�Y�������+���n���c�2}�IS2nZ����>{u����z�:5�^��TL�+�^;�,�K��=K�1�3�y���x�R�t��H�L0-��$/un��X4�`.�i�����n��2r�bI^��X�V�Z���:5I]�v�\��#0U���i�$�����\�����j8W=C��L�oL^����9v3J���>yy�>�����7�c_L]��U����15@�.�������u"o�����K�E���)Ev�KM:�L�!��K�:��e*���z{u�M��:���eiU�j��lq{�h~OL\��J�U|�J0F�%.�f��U;�V��R��KiK��hCA��]�1�eP���&��(���Lk���i$4�&1����B�������t�juw	77$�|����������GM}�,e������������'4E��1d�]4 jaC����R3CC�9����x�z��[��[���w�4�R���%��e���V
�:�t�2�����!�R��s�^FJ��������;�>,�k�r�*��1��[�D�t_b�2�n)�^����*�o�(SBZ,~X�e�2������(�_�������/"
�.�w������0$�T5��!h�5m_��U��N^��s��w�(1�������^���d��d/�2meo�s���|{:�j���e![�AaO��z8�J��
�$ ���7Cj�jF4A�!t��d�24�4�Yt����W�^S�~�2r�>�d�����
��!���h?������i)+���D�>��O�kF�x��C���������4�]��Q���R�W��f�V}��(c��� }�Q��9�
v-	��M1��%��{�;t��{��2*���/�H���t�.�z�O�.e:%���f=�����97��!zh��;�����R�!��]:���s��&����vN�O������1}&�H���h5�.��O����8�*}=�����a��]��L����;��:������0���)��3{���@���J�@�m��d����>-J��*�t�z�F�����i���b|.���7z����]Pp��K��+ySob��~������1]��N��j����Ti�l�����}��*mW�M�X��$��6����=*�}����P���m���$��N�6���,7��xmG��?n�4<C0�5��RJ�45y�F��5�u�RFuj{W/�);�
�D�e�j@�ZmX�����U�tV�6����R)}�kX5fr�{0��uW��1jl��PI�@\\����H�5N����:c�w�j8��v����W ?�aw�-��k5j6w�BS��]%��{��w��$Q}\����i�%���J�����J���W�
��]��r��r�����aS�����6�
��+���es���Z�\��exb�5���j���5WO�CUf�Z�tF�������8���u���DOi��P�CY7)�6;���>g��z��nb��-�u3`a������n����9���{��������Yc��g[� �RE�j�oj4;�O>���Rw��'L�qM����oc��M�]mT6��ho�8��}��k7s����~])[5)��_\:�:W�)�����|�s2�	@Fql�/J��nn�"V�Vu��H���	�jA�]V����;n8���r���N�"�)����Mb����VT���3w�H�z�Z���`L�
endstream
endobj
167 0 obj
   10795
endobj
165 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
169 0 obj
<< /Type /ObjStm
   /Length 170 0 R
   /N 1
   /First 6
   /Filter /FlateDecode
>>
stream
x�34�P0�����
�
endstream
endobj
170 0 obj
   18
endobj
173 0 obj
<< /Length 174 0 R
   /Filter /FlateDecode
>>
stream
x��}K�e������3)8�pei�=-(]�=<H�vVv��6T��o�Z�����9���C���>i����!��!6Z���?����2��/?=~��|���������1�X@�_Fb��<�^ji���?~��O�)������?������y>>��/�����������PB�}|*�B�����o�������w'��g������1��!E�N���'��������Ea�?�
#�W�_~����P��3���G�E%�Q4.u"���-(��bLp�����p���7RH����RKg����Hy���{��(���dx�f���W�^tji^x%����(��(�F�0�t~�9�TW�\�����;$������������N@�1���9���7��h��Sb�����AD��������"n�cx�wx	����({vXnJja��n!`t���7�:�����>�Ec1��EVpM��p����@"?�&�)ndN�w�;�"|'q|�J�#�F6;���;�~%����d�d
� R�9�Y�*�;���s��=��d��P2R�������$���@�`�b�(����0�.��.nB�{x#�����&�F�y��x�S����uIv���.��Z*}�eck����(������dR�
��H-D��Q��e��<����M��6O���d�6�/�D�@n�&4Q�!Q<��J�RD6�"��*S�=�lf�8r���uA��b
�8�S�f�q�d�M��|��A��ty��VE��2���:Y�� 
\��@�1���r�SGwt�;6V��a���y�����<r�iu^	k��s��l�W�&�����#x��l�,���,1d��
�
�2i'^�*k��6?H�����'�
����������9\���1�~p�m���=X������-��d� u�Xi��K
���#u<pcr7���:��84$�if$���^�x%�G����<�����;YWYg�	�4�:Wt��JF��}t��><(kC�cb�&����
�B��S�e`�c"i����s���()����`v��Vz
�rp��o��x: ��f���n�`@��M�)�_�f������?�#�1��(+�����,
<D@�T��������D*;����t��U�t:)`glhq��������)�	 �*����6��9"ae�iS:���"`l���j�����$�H��[��"�bD�df�v�h�$�h����R^�9g���I73�9�91���u�#5/���'��2HdA;�����M�^rv���C5=d.��W��,��q'k������p���RE��l"�9yq0�(>��,�F���#5#�����:5QX�B2�sBN�����@:'������U���d_8��C�F����P-��A�c�/6@�����?M7�S��(�����dt�;8�6'��>�L�K���N

��E�qs�1�!�N�?I*�+$����!��u��M��E�m��8W��k(��pq������y�F���}-�V�AlC}|AE�{�g��}�25�R������������c����A��#Qt�w�?m���5���1L��m�c���rhT�;j����2�O���4:�.-)���9����
	��'}c�����g����Q��;;m���?��{#�ka-����'���u7H)�n=�)[T��1�;�
��-�Z?��?��	���AEH
�nI��'��.42c���?��L���J�/��m^��H*;n&>�����:A�����-��2��������n��~���72�BQ�u'@N�9H����,�s:���Rv�����L�E�n��;��$e�A�!`�5B�p��#��8���#�J�h������'ot���_�����@^������1+��/�!��6Md�a��}��Q�U`�M;'���B�<����t���v���%��Z��� ��8�e�_���r�����}4V&�A������-��k�;K^X������\`Zj)�pR��g�h��m<�\T����[~�������N���A��$*)���7�����^%�g�I�
�	�]�O�)�b�v=�H���Sv���<Qp*��q��!X<s���G�*�����?��t��������M��T�s%g����`S^�#��NM�����&7p�OF��nW���'����uN]���A���	I���3��'������c�mG�c��i����?m�W'�JCf�j����^�#��+`����F���/���?m�W�����������q
�B ���!��9�������]j�����ude��A2H����q�t|�����s���X_u���1T�dlJp��8u��	D)w�yu���������\�\��c��x��d\�v��'A��:�����&d�i:�?��x�Y ��D�q>>��]�}9����Pr�����hS��y3���-���l:u.4{A�!��ps�I�}���x�J�m"����En�a3��s�v���N�<mCn�7i�e)wi��n�W��t|������rk��Qw.qZ�n����S��Y�+G��3�DRH���r�� �1w'Y'���;���{t^'���S�!��8R��o�������:����H���I��������n����oF�&q7�+��B"����!��R��!?�cL���B�-�DGg���=!QA
D14m�qx.��#�+eLS�s��L��#�G�'K���sd��k�;G�IPu�(��#���#��, m�����kXm��w�d0J��C7��� �?�����F��nD��Y���6:�f*��|���EVG�[������L��]� ��&V��{���N���r�Bij��^�+9~n� #QK�����fvrh<�@�(��&�)�����a�cf��z�1h���k���c���m�A�0�uu�+�zb��7����'��Ryy���(�u�/[8��B�n:	���A��]!W��p\�@27{����N��0t.�����Bz��q���B-�3��>z�W�����;��l
-����P5��*�=
oH���r�w���7\�Z��!���<6`�2d�}t�^�����M?��w���=��?O��d?e�&��(�$.�,WT�VO��'4�S��
gn��$Rp����@d�+$��&�<�^���ha�k�b��=D6��'[�+�`��O�~��=R2��9���q�:�A���y��Ja���;A#���3�4�;��L�#e#=�&�����cgu�����C��I�Nq����G6��C!g�F=���6����C��$�N�����U
�X�	��x�0cc���$��r�����Q��;"c#��?H:�d��{q/��|?I��c�� ����8��EZ)|��}5F.���\|�$���@_�K��<�N�������7��U<2cd����?m9�u0��0���X����8�#�5g���O�*
P/�z/���o|����z�l��1?��d�g���
��'�B��9��%^�	�g$/rB��?u���(	�4��E�W�;o�����S���Ts�����+8��Uc���\i����S��*��'�a:����	O�h�\��r8�b u�Ye��yV$��;�t�G�:�4����$T��zb���d��tCZ��
/�� � Y�@�y�\����a�t������?�jtWH(���{z�J�����:f"s�(��+QlO�(�����<1E2����\,�6�����_���|��C
������~������e��u����tFx|���x����|H���
����FP�����������]:�=8!
����ju�t��'G�"}��#��$�;:���DR���ts��FR;� u���N��$�V	Fd}pz+L�����:9��P|r��-��Of��?v;�Pq�|r�f/$�����+����x�p[�_�'���
e_#�O���F����������F?!�n��>}Z��,O�}#T�6I}�mii��3D!��������E��h��32�fdyr����`����G�^��i��`��s�J�������� TT3�<�d�7	ZH;c������(�1�Gg�������+Ji/���&lyt���0�:�On ��fF�'�*5I��o��y����[��KO���<ON���!�G�~���\��W�W�^���y��f3���m���o[��
N�/xN�����Q	i�����c�VD5�o}t�d*�����i��ly�Y������1�8	�������>|��M{t��~��Gg�5��o}t�:-"L��=:c}��-���Y����0�����.��d��[���J�Y@�G7�N��glyt�Z�7�����X���Zn��Xo����h���`���zl���Sr{��GG���~���m������������#`^����r��>:c�����GG���G,�[���A���z{t��9��1�G�~�����Gglaa�G���P����)h��o{t�(�H�r3�<:����z���q���lq�G�~k\���I�C��N:r�&�Fs�&'9}�f�c�"��v�2C~����_?t�c!�����x6���X�>|��������������oO����t������p^8�J�����o����/��x��B�/�:x�L7M��PAD}�n%!�JyP_����g��8�6�*][�M,?	�SU�II��H���2_>}p�iA8WZy!��t�Fi�L�:+�	_>�
����a9i��� ��(��M�"�0R����:5
*a�B����M"�� ����|��3�h�� ��������&���S#�J��<h
8/�_���hJsE6/��XH>b�#F3�W��F�9t>HY�4��N~�s��z�34",�:i��l`?iJ����)~c�)�>8T�@"������Z���ti�T��?;��Q�s}�yj�f���f��V��D���EH���%��i:�{*a���!��u�&�sy'�M����d���m~�N��>���}�D�V!����i@
	&�f�5����"J)����������?���~��S��j�u ���B)���#��c�"��<6��q���
j��i�O��W����F��--�O+������S����]�(�����������	��A8����_��^��4�f���];u�.�*��w����2]����t��L��Rx�i���#��$z�M��y��+ko�g��l�
��@`U.������h��~�A8-=��PS����#e��z��������J����OX���LJ���A��$	p&�E�8��k�<�q��4������3N��_�L��M���FL�Dj#~u`��.r�k�_�f���@I' ��nM�����b����!������n5Wc����Un	(�#'�e/"���M@h�v��*��>V��s��c���$���"�K9=b,^����Ve����u�MfQ��{E�k���Y�U-�@7��� ��9��Zye�+��x��5�J�R \�:��h�������|���~������Z�r/�:�[��3O����a+f�1&<e�^%��]��ln|Gz\���T��]Ai'�c����3�mI`�����V�����aP�f�l���NB���q���hf����are�R�]�-u�-U�8��aR��|U$�.	���H�=��cT��sVZ�n&�K���U���������u�l��B�:!�XPe�aa�T�^��VWh�_l���KIN����O���*�SD]����?,y�o�~7���/a
�#
�z�y�#���*u�������',��q�\�� �%�����B1{�RKD��E�l�76��+����������JW
;h���A��	�d���d�]2�;�)�|�]29R2�Q6v$!��hQ�#���
�������E��'�}�Q�EQLaB���%�+��S)n��![�S��a#!^���8��a���;�~���
�>A����J���Ji����oO���a6�sU0�t����$u�� ��JP�\	���7�D��4�h��+d�FB�%?��//T}=xz}�6o�h�����e��+�E]P�����
��H
��&�i(��-�ll�_�$jO��5P���1ir���It��$$���$�r87��M"�M�J���T��T��=����t>8����	����x�xt�j���M�TY��?X�^�b5�����^,m�������6B���h�z':r�?G��1c!`���P���%��:bc�n�����nr+�����n2h�J���=s����~����J�G�}J�0����~������A>~���SU:��]H�FU��x�I��a���?���f����Mg2�KW��}�����&qq4}�������B7$aF;%��l�����)���k�t�	��K��E�����"�&�{������!�W���l5�l5��~�B!���a��n[Y���r?U�����3�Mh�B�2 b[@$#�����-����^�����S}t
��S��:��3,�6=����2n{����(���RN��hz�IS3�]���AO36�d\�n���k������:���F��V�s������LS)���\-T��U�P,��z%�k�S'������3M��<��<���T|at�g�:��>��{F�Mr
���fd4[2@����V���|��5A�|����7��o��j^��@�E���+���fz��9�1�_����%ja��7�G�J���kF�S�`iE�`|���/�gP�h{	ZY�2k���	l�2�R�T��w�`"Z�P�;��m�rZ����}��~�S�]��
3����L�m�����X2��O�����y v3��
���E�l?�`��&��.@LR0K��"�@�[j�y��{��&�����B_�%��6�c���l��F�`��K�D����<��r2;��K�\��%-����F�B/���~�#��~�� ��UnDH���%m��Uh4���YU[����	�,v(���=Hn%��}[r�t������X�����H��q�������V��E/�����H	��"�<�.2$�����Cs���
B6�����2���B���	5D-��1��]��@=��MY�i�k�e^o,f���c�!,^��U����������3��/9�R@�6���=f�bb&�5�����%��T���d~�$V����`�������:��.e�����})k�KY2��PoY��/eBF�K�;M����d})W�(Of������`X�T���&�X��4�,b�e-�/�.�h����L:�?���q����������_
�_mJ��pC��
�EM�������HE��V[�0��Cd�\��Ly�^;M��+.Q���J����hR���ad�(G���|�s\������q�d%w1U^�9�����P�����7��g�&3������q���J_}���REpMjh�~j���m���o/���c�cP��g3)`S��.%F������x��5�9k�	G�VJ<�� �I��0i���N��@�h����U�	�
�"
SJ$�
K����F��!HTukU��lL&% T��L�.Yf�Q��
����4�'���l�<�1��(��(�,5�v+�9�E��P�����S5��K�Bw����32��B0=v��8i�W�(�W��������z�g����J�M��v�	��������[n]`�V���NQ����>.�o[1�N%$����b���-|=�cjB/B:��L[z��U�Kx[]�o;LS����<�i�\���~�.��me���S��|�0v���������jRF�v�`W���m�MO,^���B)��hl��q��9v���s�7W���]�]�3���&aP�*��i��[l�t-&�N���=���MQ��8S��C��
�P+�j�����M��a~�fp�9^# ����A����������b,�"]����r�&-t>�W�Nd���$k/.K9������*,G�b�eG�`�)>���(/��Y^u���GDgG��d��/$u.��5�k�al[t5L}�2Jap:����9��
�T�U����K�P�::U�N.w��F_��%�h(��uid�.��g�"j`��C���[=OmE�
/�V���)[/j�fJr�c��Q'c=�}���<�2T����N?j���!��J����w
�����~!��7��������1M���-�������U���|c4����J|v�J�^]2`����N�-���lc�$�����B�P���lc�5k�'[�2Z�s]3\�eC�0��m���Er+m{�������p�����^�:[:
�SJ�R���B��hv;�
�����R?�Sml�V�
8�������DT��6�,���1�I���0�� ��i��8��c������>)^�1����,�1�mBrS��e��}(J������n����$�6Jm|�R��#����<�j���|��!�v�Vz�M
���m!.�Q��O�9"�c��V9��
BC�6f�K��z,9t��}M����&��\�%�n��Ae��b���v(i��E��`>D=lY�;H��#�l�����a������{����a�$!2��!%�e�j���n��)���uM���riRY��qdFx���&K=��7�x��1�X�e-c�'�U~�0�EK/��t^r>�s�<d�iXfb#��4����[�5��i��{Av�wY�
�^0��l�)9��]t���:z8��k%�9�m=`R�����!���QL����l$T7}�FjC��O<�8������+t���tK�(EI(��=o���1��|>��o�	�C22�^������%�5��]H��->�u'k�C��m�FJja�)�zL,��r���W����\�b�����FRD$puB����W�25��u�Ill���bD��j�jZ��k���^���4:IF;���Yx���v/�#I�����fb_�z8o��������.+���b��f[{G���+��b�E��7k��y��}xz�������VB��^�-��S���c].�i�&	��Y�o%�X��o��q*i�����I�@ ��jRqk���4[U��V�knr#BlO�@�{�-�V��e���	�k�/�uhy��j�8�����������kX���z�}���;*��c�jIW5T�/fG��j�>���������(K��cU��w������c�5p�B�~�
 7��f�CGSF�kTz8�H����������	���^d��@V��@��W�y�`^�b��z9qFW�9j������T����-*	�����#&5Y�{��xG
u�����}��}9���Gz�Q���"$����:�{��my�t�F��gUq�/5	�8�
X�81G��m|e��-!3�����������^���D�d�#�@���y��^@l��	m��*�@=���Xf�hS�p2D�������
��n�(��'�lz}z$L3�7Z�z�y9jA��^��������<�
�R����g�����G������f�AN�E3���P���=����2m�r��`�6C���sCm���]�Oo"��S�PJ���Lax$��r;�m4��P/����N����wiE_��w�:��?YZ(f:	I���v:]��;�9f��0���M��1�n�����A��5\u����i��v�������' �P�M��|8jg�	
�I��N���9Nw�e�H���?N�v����6���~���5d�~�Vb����B�jku~9:G�n~C����Pz�~"q�����$��j������>��Y/��"y��+���005�?W1��}�2a�����2 c�Q��:�|��E-��0���7����`4'2��KT��<8�9�����k�����^,��-vd�e�JH,�)��
N�t��B�e���z�_�fy��0�P
�i��O7��6u�K)������{��e�7
h	�q~������m.���q������^�c��&��.����U��)��v�A���;)����^�-�����a�$]����,$�*���O��A����W��O�!��I���'C��|����t���d���p6�G� �)��{O#L���;�>��H/�L�N^�1�X?#�<[�P5�O\�����c7���(�v�%7b����w�^���:SvC�j2>�1C�|�Hnt�f�^T�����[���TC��
C4������M��\y��n�{��{����C5w�!�ZL+8a<�}Q�@e�R	��^�mw��!���&c�[wP��m.���7b����Q�[^Q�^qsW��(>���x��!�oO<���m�����D���l�7$�������z�0�o_�tfz��8�p��k������-W�k�*���8�dM?+�j�WGr���L������`xj_����v�,�9Ky���|=����������|��	;�X@�(���!��$�/��^
|����znF���~������*�
��5�m��S��}����5=��`@��9P���Fg��1,KJ�5�������9W
endstream
endobj
174 0 obj
   10736
endobj
172 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
176 0 obj
<< /Type /ObjStm
   /Length 177 0 R
   /N 1
   /First 6
   /Filter /FlateDecode
>>
stream
x�347U0�����
�
endstream
endobj
177 0 obj
   18
endobj
180 0 obj
<< /Length 181 0 R
   /Filter /FlateDecode
>>
stream
x��]K�-�m���8��F�����@�<.���,�����cN�}@=(�J���v�����w��G�I��C>��M=��%'tp.���?���E������7����������!��R����"���}x)�4�����_���M���z|������?|���_�������������w~����_���=��v�D��������~�j�_~|x�j�27�����"c�������&������V��"������O��"E�QK����|������nu2)�?~��J�����'`�a��MF��MZY=��<����� �1�������p���V�
�������Y�
&�lD�v�[)��4<Og������"���+���cq}>�k"}����H�EY,��E�j)K�7IL�������������H*x�<n�_��n���ZNA[����;<\�*��'x�|�c`�����H�`9m�nmLi��AG�����t��<c�2��M���ymY�l����#9x���5*C�
2��FjG�f�q��|4y����r��?q<Y���v]$�t��2���J�Q�v����8��4����b
���lR-�/�R24�W2A�D�k%�pF������i+�=L6���['��*����,'��r=-�z��+C�3���l�p�:6�X'��L/�I�a�/B7]�a�E�-�h�����*#�[�h�1��Y���RD��^���0M*��F�k��k*d�fH��;rt6�&"�?FR�>��(�.�l4F�@��WK;�?F��^/xIl��<�?F��nq�#E�4���{�w�Y�;dV�	��t����.�hW��$��;J�H�"�T���c�$R�N��b�������t����$�����2A��=��~#���(�q����9$�;�����=Gdx���0
i�G~���� ��k�5�f�;��O3��2]H5
l�i��v��Q,\4$�.&��5y�$�+5aP�P,�`���xJ�5r���)���b�i��G�t�M�;~2�N��yZ)��Q-�8c�:4��]�h���;v��]��g6��'
�rM���HF�&�nCv�L%�5k��+�F�mw�)O	l�����?�#��#G�S����h�i�9�w����{��7����C"��m78��f��� �Tp:-V9?�@m�Jy�f~�����I��3�LS~���@������;���J����E�wN<�)�ig��Dp��\efE�������`�����x����we����{~��k�n�6��0Uw=��k��{gg�����@��N�:�l�n:�,�l��}^'�^�F6�$�K��e:��Z�����L�T�����W~�z�o����=�VRX��E�"���B�79&������H.?pz��vpj:/V$�y��l,1��8����>-����k��f�Rga�c��e����H"�0u.K��,��[���U������{���ftdjW�$�Y����.
���DE �Vu3u4�	~w�hxM��r����M
�i�: F'o�o�c9`��I�9�u�<����Z�	\�O�qh��5�����\�Y-�q$s����9��hg�"2ilySc���*c���I�+��r��?F�x �w��#�5oe|�I��8��D�����N3�Q$j�p�d��d�^9������ivgh��.F��,�����R���Fe���1�K��"��H2�-X{���8���.#�c�S�u�����1r�9�.h��@b�#�$����GL�i7@����t�v�[��$-���f�~BV��z�����]�+@�\G'��W-���lI|���)���d�TSlzl1���D�<���Yf�BJ)�IK3��Pz�&�
V�doUrYeDib|Io�3pv��8��L�&�;��������������%��W��*�����7�c�{���������'�^dj������^��A����pn`�j����R^E���m�&r��9FXU�QA��cjw�����,�N�4�����R���s�E���(6�17j��_C������k���j���9�A�,������Q"�;���v$_jl�[T�0_�/�$d�����A�U�����o7w��v���U��u�(�K��;����!��6<���1������l���O5�~� ��"t"����c�������["��D�]�{D�}�fO����/����V�#p�h�N$2����H&qi�Ep�'I��@x\��W�Yl�Hg���18�w+q3��t^(C����d�*ri���������E�1��7���}`gp��� �R��[�;�G�O�M���	���:���0N���)��"�����Xm�:D����>�~1�T�>��`�]7�:=�<���)2A*�N�����	�;�����d�M����@x ��������I�~'UCo�:	|���S@���4#�������>���~�'��(�g����Cj4�<�dS�n,W�1�`���������Gd_�'t�s�-�d�-p��8�d�7�E���a4��m��Pv}��+bj�E��2J6�ic��%�qR����;;F&F�NF�A2[�D'�e.�+��O�v�gFc/A�Vw:�$\;q��83�IDw��i���oJ���/���w���@F����8�$���i�PNG��S��1��r��B��^*�b�L�1�w�tL���L'��2	�
�j��X�E�7�x��j��"[ 7��Y������l_�PVX��	�v�c&�
~r��8��H��~f$�n���"W|�`��R;SI�H�����3v	�����u���O����+]!}����7�7&��{Xm��lm��4������l:���>��"����,�q;��v�tu����uw���$�$�__8�O�w����W�c=^/�GZ�����cD�}t�����7�T3��2FDOq���B�88��a$�����H	�c��`)M#d?$dl��6l�8�&�����sb�s���#<M>��ZTb3F�(�n�'Gp����� i��.����n����j���k�M��i4�@�w��?7���:�xO����;�Nc��@LF�� \�	�Hx\�d��4K�8�e�,��}#��9h��[��d�3�� V���2����M���@��6�88�C�F�N�nf�v]�v3nu����Gx�����6��Z������P;gCS�]�;cH�7BJ���>;J7q�F���*~�G�������c�z�	�[ew;�����]U����3����u���?6c��Y$�s4��Y2~GA83A-������L���!�n�m�p�e�,�Q!�����2~>9�p�����d���0��SE��J��]H��P�2�w���9���[�����QC����M�n0W���;�����i��5���3��X:�(w�T�@�d�G=H6�v�.����s�+jp��� ���O����rl,2G6���F��+$�i�;�$���4������;����)���Z�� ��0�Cw|"��
�[C5���:�P����ozau�����MG��I�S!��9��#<X2�ic�nq�Y����o��"���Ev��N����k���W�v��n+� ���^p��n�J���q[��2E<��R�eX��M�|��t/2�����h��C�)O"<�yw�C�F�F�w�:^X{�c/�8
�����;>����|;F&��d;>oL;�p�7oG���Vpe���q�n$C�����Z���@��7H��-A��|9���sI-Jc�x�������^>��B>~xQ��/��>T������:/|J�7oEp����=p�m�����x��%8D�x9�FT9���=��T���t`{��$���O�Pg@q���d
�
5 ��PK��/�H|��j��E���k!C�n	2����i{���)��������MzD�';���������'�B���(��>V+!���+���Z����+���X�W�,�����d\�����.	�}���'[�
I�Gd}�EF+T�D�>��������6L��d�g�"�i��	����8K#��:m������l8`��=Va��������NM���6�$�~��-?�	��gl}���ja'��O8MlFa���O�}��I�����Hj��>����� �������O{��5*a�<��h�
�p[��-��cNN$5��>�b��PL5`�����������_�����=�����r��=��Pk��	[]�7����h��6V�8i��h��I���}������
�&���9�I�%��<���Y������lz���,{#����>����"D7c������A�[]�F1�����F�&d}��3F���k}��&#d��\{���|�b���m��;�u3�<���QZ89��=�������'ly��j�=�[���(�f��������<����K�]�opm��W}{���B����h��.o#Gl}��F)���\���	'��m�p�� �=��$gG9c��}��&�j��X��J�\�G{l���j�G{l�B����h���|�|��Gu3�!�~y����Q�+�1����!�����N���~~���7�&������O�����O������_�=-\3�������20Y�o�z�T"�WX����Re��Bj������tz*�D�:#T�����j|}3^�������;���7� 8���o^�L��<�?������}j~���O/o����M�W+�S4�T��m�� �WRc�A���������W��:������h�@m��6F4
ksa���U.=��	���^��S�q������6�}�&�TR��K2?\�zt�u���T�����72E$ZI��������N(c����~��,������{�o��K����� �p�&���V������ ��pU��W��L�����
3�;��S����mR)��,����z�|U��i������{��b��������d�����t���i2�I��9��k�u��_��r���_�B�����n�r=0	o]f@��@��z:�p2�����!I\�Z�W���HR/D���R���J(���J0Hm��*a��(����K�$�'�W��F�Oc)���NQ����;b���o�Z������C&��c8���f���#���CU";2i\ �Gd'�B
���d��+���D�FH
h����9�~��)����Bv8c���#&�c�����UJh�������U��TvR8Y��lg���G����N�P��n0��P���
X�V�g��_k��C��+���j���FE�n�a���������?���+�
O5+���� ���,�R���lB��`��Y��Kg����JUUG�}�V�}�4Y�h�6���!z�{������a��Ii���D�Iv��bU�R�a�����Y=��k���u���`�.����^o�6����Ok����\���jt�@�
��Fu��0�2������u�*�usEl�3%!��\�=8.��Jc�9���H�"���,�I?�Fv�G�NVM����%����K�����Fp�����4N�����	��0]+��}^��P�(����3���JrB�u�1���C���Uu{�`�-��zqA)����uB���o�X�����yq����#����<����MU�6� Ti�a���;)X���}[���\w��t;�V�8�Q�P�����f;�N���9_7oY����Q���~n�X��]��k39��`���EV;�<9U��[��*;��GG����x�������;iqA��/�������BB�%�7u�g���m'
z8��Rt��W*���B[ha����H�N�����&�Y���q���`�6��C���
�����l���'7x��l���i��
m|�'t��&|i�J�*�������N@����1���{�t���qQ}�=)�k��U��zkU�4�Uuq
8���\����fU�T�UM���kj��(��i{"����Fv��=���d���W�-�@���u�*�;��BW6+��Vg����^��
$�6�\�E�rT�W�JR��=�U�� ��
#�y���j���P]���	�q�f����U@����P%��#��T�$"y'�G��&�)�9k���7�R�]�8�z2t�F{����"�R-C��{�?��@P	g�@P�� O�\ k HQ�{G"\�`���������Pd�(�(cRm{��z������Ky�����#W����2��5�V�f+�p-"�&~� ����f����m#�d��g�ha�����L��`�	�,�;��,z3��\Wk�8������( �R�F��K��{��g�4�P�����a��!����l��Zi)\���K�s��A���6����fW�
f�|���]?�c:81���Z����������H�����R_�IX�`�L�G{0V���q�q���8���x[������
V�>z01[�0�it��;�A��V�=���M�������/E�Fre��R��TG������}��:3��7����tD������&w�a��!�2���{z����}�����0y��ARS,���G�#�=�T�����b�<�HHqG�}�f=�$�1�4FB"x\5R]�y�w���+cO������>���	f�������/��� _&F�e����+�R��x[bT9�h�Y���I-��s�=����������8H��1���s��"�����b_b��\ph���1��<�Q�E��_~�������w���)N��Y�t���N�,�-��2�N�6����0���"8���k��n���U���]�i��S���
R ���4�2]���u�e��L��At>+k��(j�4d:|����w�8E��*u�	�48����`��f�5��������(��j���e�u�*�����;����o�����Q���>�G2�f�������SZ�N�W�d����b��1dU5U�US_Wnam��-%2%i�P6���)��&�T/�RD���e����,	�n��eSv(��C��"�K'���)U�@n�rJW
��}+�r�H�a����������5��0�=��+MBJ'�����G0�q��y�C��>�Cp\�a�0��"�"�:�%J�H��y��rx�h���ZZ�\C��������r��	�$A��x�zm%>?�5�Z)Agm���2iiT"�|�QI�Q����li������E%���B�)���}�7c ��%�5q��|������J5w7�	��N8,48�,����.9CO+�z_�.��P��)d�C�����<Z������?������}��t��~����1������:���M���gM�t{��rH�V��)� 0��&���o�N�	�V�s��jZ�ld�PH��%����u��QmT��QD����A���DpE����,�.h$4�RX4�&������F��=	����\K�e�/{���-n��u�$��}{��D�����#u��k)R��.��u���p\@o
��}K"��7��x��2&����}��o:x��A�P�R�y��Q�!����k��a��3a���Z=��5�j��
n�Y%1������m�\���4���P���N��e���9���]5
�c
3�:i<��<�3!��j�,+�8�9�d1�T����`�Y�������l�S��/g�����nkn5�&��Bq\��Y��O&[���eGZ�#T�X8g�N<���b���~|� k�ene����[R��u~V��%f����������\s�z��:��rj�~��%�0O�V`�Z b����
���t����ynR���|����N�
�����:����Le���e��A�T��].�.?�>���������EgmvT��O�tH-��x�m��wG=�����+�u �����\�����\��(�������59�X�K"�����a�R�tf���e���V������[��C���h��8���Uy�)Y�~=+�f;��������~����C]����3���>�����/������!��pp��>~�����N>����>%\N��i�p������kz�/��� G1��#���<�Q�i\�LH\���������kQ`�PB���Q.�LS���HN�N�������6"������jmwS0Ubp'��������: *����|���N����9�[A��P�"&p�S��%��{%S� u���M��i�{%�/{%�^I���U���T���s���h��w�dP.��������4����<;jY6X��%���gt�^�XZ��ji$�OuQ����:�>����s�5�9��	���i�~d�7.d�]u�A� ���-����?]�b��K��/GN����a���<,q�%$�`By�rd}���<��(��*I��	�6�O5Vk�������EO���8Z��:�vZ�)�T��>�&���P�t��Z����v�~a�A���t{'���kB�-��/���i��W�H���ZTZt�j��i�} j���}�n�P�\q��N�b{�GV��c"�����T@Z�)�@�i����C�f��Ip!r?�1��z�~g��fD�k��
SEnI���6�?�W��#x�u� ����e�������\�-���u=�/������:h����KdK���FZ���>��v�Yg������m��C��7�$�m(v�W�B���c�/qzD�3�T|����c��E�Ob��"���u��3+oh�z�X���������������V,��K��0��}\�:=�aZ���xo���&��V�b�+M��@�cu|��c���
}��&iat������q;_$F���?�D��:m{M��#��#��l��%>!L���m�&n>Bj�m�:�0�X+���;�����NL������a��}���&$�<jcm�z�2�L�Gd\�:�m;�������:�R�PFP��0r>ow>�w��K�d������p�G�p!����	����L���g��;���Ee`G���4��,S+����E�"uM�)���7��,f���V��1�p��A��7�p�(����������Q��$� ��J~��'��G���=���P��?��h���q���x;���i4z���!�%����2j�v��m���~������U�@�,�s��n��J\��i�,���P������Ni��M#x�������S���L�{�s��$I�78�q�[�7<����.���������l�L���$�t���F�������+jz�
cD{8mv_O�vg��%�H�h�����T���v0v;����R���S��
�}��L��]��y��C��|��DwU��Y����I�����0l���r\e�?���-{�"���=�q��U"^��u8jn���8��Jb��s��]�%��x1��z�x����T�N�)�Ph-����i�����3�Qhb��k��J^���}O����=����$5�s�G�M9���S��%]=�]jv��G�L���<�%C]v�������jWL<@m���
C���3��:�p� ������]�������\[�E;�E�� �*��D;���r�P>�����A�������D�1�����GE�|��r{��������y��x�?:���z`�g'�B)�������&��pn�lg�3��>J)Lo��!tx����c�	Qw�r�*g�N8�}/~�ny�)��l�����Y���3�g1nxGk�;y���.[�M�VX�yQ~�&�	.\
��h�9
����`��������3/��r��L���rkHN�U:C�%�(`����RXrHE�W��!=��[&.y�R�z�'���2H��w�����,�Z���;������e3������x��^�����7��v��{��Q�i9�x�r]KD�WQ$c�������"X���^�����8�!Wi�M�r�l$T=(�+��|�'�C@�V"�yV�jHf����@}o����	��|����"���|�z����#U��u���z�Z*����2v���b/�"��%����3���*��;�-���^��"T��RRrJG�)Q�{�t����L�H6w��wTiU���)'y%�~5dx_�/�#c$O���T�E��T�E����
��%-4�d���deC���u�G^�����[Y���GuI�8io��`�Lb7s�Zk���+�
f�XHM�"���U��:��UPvH��tD�4��%i��d�D"�h6��~]�����g5����&�!��c��������"[R�{��"�b����a;r�j�P_�;s��p��pa����o�D8":�B�b������=��9�5�,�\6m��cv�i�����s�x}���e�e����ekP����3��,������#�#���l�lI�F-W��PO,���p�`����g'Y^�)��X�d���L���?�<��p(*�������1IBa�2�z�K
R�����J>���������~_���������k
$�:YxkN��P|NK�Sf���b���$X��'��J��S�u�s��)�r��>����i^��!��uD��������}�Y���<|��PW����(i��k]S�%�J�����R�r�N{������k�i/�u��
endstream
endobj
181 0 obj
   10752
endobj
179 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
183 0 obj
<< /Type /ObjStm
   /Length 184 0 R
   /N 1
   /First 6
   /Filter /FlateDecode
>>
stream
x�3�0R0�����	��
endstream
endobj
184 0 obj
   18
endobj
187 0 obj
<< /Length 188 0 R
   /Filter /FlateDecode
>>
stream
x��]K�m����_�'�}����4m
��N�����8	q�q����U�ZKUZ��k<����H���-mx��~��C?|��D�<�����o����|��[���o������B� �/�QaJ>�G�*j�]x|����x�����x����������������~�����o������J�x��E�����7�����}���#�'����7����Ur����>D�>�S!��q���S������J��@9���M?���U���i�c��M2��b0���V:	��w����X<8e��)h���G�#�������F�U��|��&S�w'��rW���D9�tv&�M�2[����1��s�*?l[1P�y�n���Z{` G�!�y��u���@�<W��&�[/�8'A��~L��NY�������;8@��@��9'�N��&0�]o��9y���{q�r
'������F�C�
Y�������p�Gh0�!b�F'>r�+����g�3P� ��-[v����'���Z���������H�:��]�3t�3�X��|�������x ���]#�~���s�#�;]��E�D��U|��A�'
�HO~�'����v�����O�f���9=��=������B�6��2I�z��X���l�NO�C�'��NZ�`�uj����=�f�S������S����\��b�����3��/O"\�����c�����z��������4>&��g�i�������!YK
4*��S�`=�N�F�����:x��,�G�Y�L]�IH���#N�wP�2~%�:����y���i�*��&�jf�^Y�d�t�Ip^D�� �N���W�� h����?�����[y��hIs�����+"����P�0i�������E&�%��:cT
��p1��F|#��>��cJ��R��s��s��S
r����3pw_d�3*4���#�;��l������@$f:R���1��	Yf�Q_�{�8yX�-?�Y�q=��l�Nd�y���M���	|
�"G�m�D{���`��5�����js]��?��1�
�Y�L]9��8
����xw�F�y<���x��f^(z�@'I��:��������v�c���$�������$*.1�~g��J����R����u����g��A���������=pgg�:�'�B������N"�3W����3G���Z�x��#V���^��`U(��#����m�<���������!p'��Ao9���r����;sd��g��]7�����yi'
,�4b����$i,!�<��M�1�G��� F��aK���F~��.2�?a�5�=��Z��l|q���	x
�����
CP��*��A<a{\y�v����5�Pr�BXPr���;Z�$%�AX�L]R�G"PPY��:��&�K��(HEi�;���Sg��X���fe����d��:xf�����,:s�� >�]�]���4��U@V��!�
�����7�K���1b>;8>�#�[+�6#���������T$?��`��^y'VY
�q]���:#�.��JLZ�(	��o(�~�����c�������p�!��w�	� ����i_Ug�4m)����-�:7
��A7�2�����p�8pS
�<KP��\�KJ�;��s<<h��TY�����:{��0�
������
9Y�����P�E��|�������}t�s���n�A\������4�zc�JqU�<�����z\����G�}p���	��xYD�����2��&ew� T�$�8�����#�9J�Yb�8G���b`��~'�^%$q�:�+�o�u,�^�Z�����9xc��3�|�����i����r���U8q�C��wf�0?�����Tv�^��,PN:��_��u��&M���/O{m���k$
��;L)���d��+��1q��J\�&�3#�`�	�\d90wB{3)3�+���!	�WG
�c���
\%�t�������|w����E�*\��`r������_|:��Q��W��=�A��)'E/�G��p�c�
1x�*$��U���9~o�FH�7��:�c�E����]#"�|���(��C&�������s�T'q��s���)i`I�uVIx����O�KE�����l|��;�w�M���z�U�%��)[(���-'�r����{+�"
K���1sT��}�p���1�p�]����>��\E���Q�'�/;�
�[��'��m�B���o'�G�UI#w�������g�<o(K�'� e��x��q o�H�SD��u}���k�0*��yUX�FVJ�0X^�s��?}f��u�	|#ra�ivC/��Dt��=�C��h�S�T��iVb�kQ\�t	��Z��f�c������?1N���@���Vx�80N�ZbpU7:�����pc&���,(t��u���{�h�o�F��������B���8�w�Sf����W�~���t���z�����k��UtF��F����_D>qp���4� ,h�A�e�E�������	�(��c�8,�����
e���H��&e��6d{��w���/�����f�G����������":���{lW���������~�	7��O��1wel_i6��GT������ :��]���@�x�����$���v�r���th�E�T�efI<D����6�k.�
��K�=�1�Y��C���Z�\����ctF�c�|���S3p`��2:��!��I�^�r�����rx&�1�:��	�W�Oq���
1hn�lrF�7&�u�;9�RJ��7e�]�Gg>J��(,:������2��|�MI��u[�*���c���z�q���3��Hq�|6����B��r��|����������x�=�.���f��f��.|�V��R�f3e����I�W��b#�c#I%��n�Xx��*(>75&����C���+�U���^mB�8��p��8����_�	�3�<:[�D��y(����������wnu1+m���:q�$�I��%e��W1�g"�b����4x��4���l�����O8�=8>�����'2P�����ZC,�%z�e	]����r��}����������!�[|�?�Q�p��
��.�1����ozwp�6�4����;��s���UH���4����irq����������������j7>�E���"�+�U0W�Q��[�1�7�?�&��B���QN�5"�5�'��,�^%�s�uVW~����#����u����o�����o~��J?������0~�F����A����������7xm�������<~�����`�z��
�5N�7�����`o�MR�����-��l������F�/�N���mJ��@R�jB9���l��l�
[`����/����,X��������������^�]���Q8n������!���Z^�Kp:\��#�m��������������tDj��]���OC ����G&�]< k���J�����G�����-[d~E;Su��&�T�sGT����M�q�U����6���snM{��*���=�Ee�Z[�H���Q[�$t���/�e	?c[�~\e�q���6�lL��i�M��?��5��z�������g�� ���n�7���#�6��5Z}8����+���M{�u*����=6_�L�S���X�T����I�?�L�5"�Bo���
�8nk�����=����"*�9��=6E����lg��v�;����R#W�z}8��i;cQ�`�������1c[�~\*lo�c����7����|����������#�Z�~�J�q��i�����~������#�����F��Glm���Zy8��5��3ze�q��i?n����z[�~��+��������=bk�v��sA}Xoo����!��Er+]�Ur��������M8`k��9��9����_�IJ�#�������[�~����3nM�q*��i����=�����r�J���[�~�U��9���^�F�������1���mM{�)?`:c[�~\���q��i��d�q����[�|�J�0��t��H����a.��k��~��Q~$5L?��+a|H:�G@�<��?mu0��#�tZY]:*��X�qhT�}z���x���wO0h*U��E��*�]��~&e}[�t���\4�r�l���������\I�V��u
l���&9M��W�����������{�>���+��~���w������ZM[o����o�`Ay� e?R��Km�:vB'��	��V�_to�`���))��X>v�VvM���*��<|��\�r~�G#�v�M��5qM�P{�'V9x��;B�|�B�p&��������o�������n?h�����������m�������
��Y+���*H}���v�v'�p/	�P"�����b�D2���	�{������t��7|J���Wk������'��!�����o��&^�h��=Z0�/*��Ae2�-`��*�5����&�$x�2w�S��O�+R���7�2�S!d0� �t����$$���u�9a�=�l����Q/�v��i���d��h�ENT��3>���n���d5�Y&���u�������k��EC�	��B��-�P�O��&��`�Q��d��T���Q,��f���&�����"_�q�Wu0���u�������%K4��!�������,!�.��UnK����h�"��X��UHN����:�S���������Z�g�!}�~_PI$CU�
����e"�������D�
vo���������3��!/��e�����n-�����/�
�=�
^�������1\����AT�����"�����lf�<�I��?U9��Q@�����y�0W���������;�a�+�?M\�'��5]��wt�<Qa�(���'*���<���-���sXI��~X�. [W����"�\�@�b�#��	�3�$���S�S.��>�5�9���>�w���H�B���z��WX	��v6�<c�y3�d�����W�
(c�X�OO�j�0AO�suN������r�N�'kS�Ni�[>M��Z^bg�����0���I����
.�W���B�����B5t��78tPn��n��vmm����%��H����H�PM�� X�M����)��@�^�:�[+:��:�����}���6�M��t}��@P���"�lq��|teTv�W6�I�����(� �F|m/���YYd>f�j�.Mp���(����t��)=T���l��R����3�����]��E����#0#o�RD�;&�D�I�N/X�ry�����
F
�EJ���v��{���L�C�~� ��z)�������;��Vr�����lLc�^�l�dBZ2n�~G�L.�q��/����\��lpb��6��`��Xi[l�vZ
����M%�4�k�I���#T^���>��+�q�:�3���B�=xL��Wf����
@�t�����h�C9G�����_������u���Z<�
WO�Ee�D{>	'��v��*y��t�eHD�&^��2$�J�2M�����)�������ck��T���Hy`�0DT�����a�,u B����}�~k�kdtd.�����r7Y���p���X�����uT�+�C����"r��M$V5L����u��/�&��������^��������L�f=������DB&rKn&�Xzv3�����#=G����t�Aw3-�f��$�a�9��j�x3H�K^H���qp#@���UD
d���?	���[^�6B '��ylSP�v�Ei{�t�NZ��2��I������'���H^�^O�$���ik ��{m>g�g7od~N>g�����K����E73>gIB�����3<g��3{�n����,A-�E�>�!�D�����:��0�P�����m3��E�Lp��W��9uQ���y�T3I�(�U�g���e=S<��sNE�����7s�����]�u�QN����o��~ �d�s
�%�P3nNFY�z���r�c.�(,�:���7�Q� Ed��G��|�����z�\]���%%~MF������s�����,�]'=t2D�d#����Q5��&��LU�I2��%!%�y�=h�[J����M��	��M:��e=�'����L���8�	��������D�!����9�\�������o�����I���=�~N6��g�r(�GPP��I����&���)��Z*}�u��L�X<�^��M}g��i���T���H��'���:�95��5'p��B�m-7�8���T!�5Yq2j\wb��R�h���l\�:��+�!�U��o�el;*�<��������E'������I=�9��1������Xu�����JK��0�7����qy����C���wSj����:�'0��'�I/DN� �_����fo�N����;��%�r�M�*�&��kj�[1`���0c��4��-GS]���
�l��U�
9��k.��>s�u��+��j9C���jR/OU��Z�D���R�KYj�L���L[F��	����5N`F���+��s��"�/����kP5�]�����ds��1��|�����Vv�Y���5�?%�z��c�qX�s�����
�x�H�-�C'Q��P����Y�����w�-�<�����y�V���u����h�zC�%sk���% �}k�9.7���;j��B��:�x�)�T�8�sVk�}U�T���X��UH5]q�HNuI���S�*���y�{��j����K	~�7���/��G�J����CO�V������MKg�5�/�[�K4��w���+�V&R�
M�W�fX���M�j��O�k1�B������g�P[O\%T�.W'CJ�@&P�-d4���d�j5�oug������S�j������_���X��qpm������9��	����15_�=A(��i�`rE��rgK���z�wz\Kk.�H�jk�����<�����:��&���Vd���`Y�B��.�����%�Z.*�����
����+��h"y� �W��M�Y�i���
�n��1_����%�<Y\Z�Es��n�2Yg��}�lK�;$�J�"�5U������u�������f�s���^#qK�l�U�#7���������8d��Bs�����s!�-��\���h���^P|�V�Z���>��9���0����1�1�n���!�OV4���J�z�����e|3�d7�M�=O��3[�������7��#�k����
/�1��_�w��!m��*
�Y;��J�u��Q�����e5���ddpNY�)]�w������3��W�S!����OQ��s
w���9o��K�)j�i�MqLd9����y`�B�K���p���Z�z�!c-����>_�F���#l5��BYgr�`i����m W��l
�����6�)�!���W�TG�=J7������K��BB��i����gr���i��Sc0�1�=�Z�P+�0���������Z���1���kvG��1.zh�.MuJ9gX����}�l��#9�W$x)�����E&�:'��p�i�H�XA���UZ�~(_�qt��Vv�z�c`�A�+�Nn���>���/Q�����
*�d����W�����3��k����`5�L�&�i)���TeLj��>,��d`F;�]��e�I��.n0���]������,�����]�/��&R_�?7M�����l��Q��������t���%��b>^OP*��1�)�[T���>H�*���N�w����FQ�||��-/�1��
�S*7�(T��b�n%�N�����V2]�a�`4��:�-F����W���	K�_��b3e�%����q�++����_�Y���R�)�]����.���St%��k6���O��{1�����0MD���$��+����q�E^���0�.�l����V�5b�[�gy�L4_j<��(�M���x���_b=�C������
;$-sJ_�~��|��WQ�T8����N�2�������T���I�5S�7h	�z$�8_�YT�|��|�������H?��&���C�����P��-�����G{���y����������M-�s��N3�Z����
�gL#�����a!�k�k����������r��{���p����!�������y������GM��\K#�p��4�n`��|�����/����R����G�[��6lv�5���i��-|U^���c/�9^����ap8�{�N���������{v<��hc]��4���{�����O�������F�
�y���:�g����^�����y�B�'7��FiC��k�p����Xg���8=l�sH	���/�����o�u]����/f��#]#�y��x�n�&�Uf����r�z����V���R�����f%:���&�2���� �UcU!����
��d��v��VY��|�������i|G�S�S��>n�3g}�
����?S���z�h��T�1��y��+�4M��B���8��������B������89��;"Ok��B��������m������B���VsU|����Xn����{@H3!�c@M� ���0��W-�M�bqd�� �R�W�Q<���O�e����k~�;����s�u�-���	�g�P������yT��<.��oV�U3 {�)��eZ��F7O9��]cA�r������91ts.O9���\7����Qnz�t�$��v^l��(m�q�q�V,�����s���Bn�c�HC�]����~���l9�#q����u��+�5X�0�O����{��w����;x�����g��x�{:�����d]y�n%��iZ����}7��tM'�s�_������\���VS�rG����|�t��5�2�D�R������bg�Q�T�Qo��Oo�el��ZR�����#�p^9-�VwR�+9�L�v-'��r��\pmk�,^C��8��b\l{��kC�d�I��8����yi������E�������z\�<��T=����Mwz�.�����x2l�Kw�t\Kw�^�7���k��+Gk�r�&��`g	���[�
?���O���:�|�9:w�}�������C{n��G!t�T�?X����2�#��d���F8�5� >�:��S8�3DU�O�q�|������\D<�m�6K��C�e�8*���U;/���6�}�k�%��H���|���^��7��W���!��'z������@�jg����.:�g��E�k������!C��[Z����[��`�sC�v&f��3�buv��4��F
�!
C�9��{�Fq��0^���9���J��G���9�3���s���1>�U�-�����Y/1��C��n#����>��������Qz-�0A;�����t9DMp�M�[���S��o�#Mp���v�VG$���*��(�`��C�t��FCKT�� S���[{��k&j��Y��{��Cg|0�o\U�d�F;�dl����TLP������B�7�O>���L��g�{#e�O�:���2��/�[���^N��\}������u��vL���<�vU;�\���a�'b)�������? F�
endstream
endobj
188 0 obj
   9761
endobj
186 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
   /Font <<
      /f-0-0 7 0 R
   >>
>>
endobj
190 0 obj
<< /Type /ObjStm
   /Length 191 0 R
   /N 1
   /First 6
   /Filter /FlateDecode
>>
stream
x�3��T0�����
4�
endstream
endobj
191 0 obj
   18
endobj
193 0 obj
<< /Length 194 0 R
   /Filter /FlateDecode
   /Length1 13148
>>
stream
x��z{xU�hU?g����y'��t&<&!!C� 2-$c0
 ��L �|��@WOCAE\!��:!>X"*������.�D]WAX+d�~�3	w������=��}NU�:u��9U�z �q���]�E���)��no�K�OY�if��;����`�f��`f������S9{Fm����L�	���=��][0�8d�����]���A?����^���� �&�Q{o=YL�
������3��v�0 �	@����b ���M2�^G��8�wT0cQ�����-�c<�Qj���7�G��?-�/:� L��&���B��K��Z��pd������E�5��V�j5�D��id�1�
���h����I�II!C����U�	�w��TW����|��8�@h���sQ�$��_`w��P�d���2���k������:�;���q��5�D��������Ut�t�x�8���&����>N������c�lZ��4�6��I��t�HG%�i�x:v�c�%���*Q���?>|���E�Ej��D��&H�M^�����
����F*mhkc�\|��]���#v��;Dy�k	}$���%)@�����X� �'�����"I��N��,��� ���,+@���q��H%�FAH
�d��`�(��\[��X_��`����,��\^�\���P�/�Jb~tfv���,�-e2l?�G���\�l����O2b���x�F�f���g�~�����]pg���p���
�6�S|C�T�)�7e����-
�Xw�����]6��^k�
����9�������&�)"���2�X�&u�Qc��lb,���3)&����u��!��v�_]�-[[����:�W?3��5��nr���v���)�����uD�?G�s�
%rg�������6#��C�T^�R/�x�
�F�f���������TE�~�_�J�e��BA*����6)�2b`��O,+���a �X���������vU�
s����qD=��� )�����<D���y>m���-��T���ZZ��I��Tj,��J��IN�1��Hk(L�[�����N�wb��!'�;����2�
��*�Dh�S`&5�i�����g��?~��S�W�z��UD������A�����}������c��X�L��ndA�<�	nA���>�+P6��
�m&�s��n�ld��l��Fw6���3;�Q����wb ��lQ��I�=����~���K$� !������iM�1;�����������6�����z�u���0�<?�<�RW�����Sl����������#�Y{�\J�,P�NO��
aB!��N=��c�=n�c�����#��\/T���q���^��=%9��R�o#������w����~�Pb�05�B9���{D�N/�}3��p��)��F%t������e>���C�y~���>|��/�p����\^�aS}x�7>|_C����|8���|���E��:���ab�F@���Ot�^���|8DC�>,���^�a���Ac]�-Z�6@b���\	�Kcz��D��s�#�Dr*��0���C]u�����9/kx~�y�etU�&)(�����E=-��{p�GH��nth&hO�4pO�����=�v$H������d��l��������^J�|�^��k59a�@:�h\t������~_� O���7]����Gj�x�\N�����I�I;hTh�Bc#��4�i�i<��B�Z������=�W���a&�R=n�Bs}��!��x���^) �Y�iZ;����	�<����0K�f�����AR��<����JIA<k��G���]��M;@��A�#�[�KQ&�|�t�	z��GP�:��0)6�<K���`�\�q��n��/������)V��
[MF_(l�
�(d(L��%��������k�:�c�d>v���|l���|��G>���1��YYRE�I�A����J;R��f1WxXu������E�(J��������M0���B=�F"�����/���C��������=�6��Q/2{J�Nh^x���e�������������]�FaNm�u������]CZv`���O����K�?��X0C�#-���@���4��4�Riiz�����IO��z�1v����k]���zF\ra�{�\��p$kW(�rHx*[?m�,Z�\w�������]����U~��K70����U����b:�o������A��UH/������j�8-�����M����t�ju4�����g�hg1�.e	�L���*�~���Gdu��"UpP��a���(I�xlr���N���������E����Cc�<����u��c?��'�����wmX�u�0��
���\���T:�� ��B�c�\��`��9��l�g���W��D��I�Lw�M,�{WX�?(��g�V���!C���!��6+���"9����-����)J��������G����c�\����Oc?-Z���E��M�W>�[�+���Go7�n�\m~s����9(�+w��{,������k�}�Z]�M��>�J�"�@S�0:l�3yc��y����|�G��~�c���b���p�j���q��A�����T���	!��G`��T����tk�]a������0oXc Rh��%�h����-4�ukMSZ"�ha���v���c���~{	�����p����������+n���%����X��x��b��w�>���Zs��
�=���c�4�-��&�Re0��,�i�pq.C�r�L��t��N�
��������r�3;r0���9�AS�J�k��S�N!���p����%�f����$�����;���X��rQ����������1�i~���5�W?�f������������'�����7�Z�q�*k��7�
1��u�����#���k���$e��>N 2�0<���<�8G�c�c�c����"�.��I�Y{m����H�H������i�<���!��)�����X5����7�Z�:��}i^� ����|$5n�4��?�A�
�����KV�
��JF���wi�o�}��V����.���@~M
�T�(�@�N�B!��I��>���8���bn"W��C9����!������#��p/�[U��z������\6�&q�,F{��C�G���n�g���p��{�T��s�[*�ln(��a3�����������F�q��!g��vp��N�l���[��pd�'r(s8�C�C�k��Y��!��q��J��)R,I���Y�4������
�j������t�<�=/i�=Jz�k��'�I��A\{�4����#�_�9�16�����H������<�$�F#�`x&K2����N'jW�t���6������,�������+|x��z�K�m�������4-����g�;�>|���hcv�l�_�}pCs�Mv���VtC�L[�o\�ljj��{�����,�oj8��-���>�$&�L5�	�A�F.3`��
Xl@��VR�4�q�e����&A0+�N�z#Nh����u%|�/���v���_������92`�M��������M�jT� �W���t�H������������F���jx������&���#�p��N9� ���7���H�������a��F-��G�@����8�����P;f��nG��������n;n��z;���,�(�vd�8�{;~n��v<d���2;6h��92��j��i���qR7;������6�^;>g��v�oG�hcfiB]sA�-�G�6N��5���5T��^����X�qbG��i�o�6m�e6`G�dG�kar���[W�`���+�kW-g�u�����*�_�j&&��V��gD2���,~�����~�>��c�����f	������Btm����O
��8w�HN�J��*2�;E~K��t��G���HOIwe��`�Ca���S�v,;2P��s�3����l��������w�+�@�^I��wI�@
�%�d}[��8����%v�v����/���Z�t�%��I�TX-���1E�BAj*KRgps�P5\��x.Q]��E�<aRi�k�Z8�+�'9.���c?����N��g	dS(OeK���"�Vg����7?�b��;��AVK�jz~��emmx���1�;����"���Z���u=����o���5��f�.�R�2\fZS������?T;���x
�D$�<��~��,�P��?�:5�k��w"z���z'~���?����  g��i:�h��������2f��I�%��	�6f��&� /�A�t���Z+�v��"
M�)���	�LPc��?��}7`��a#�*�xs���gO�|��c�����;�S�\@�x'�Q�QZX����x8�e�!��^���)���S����w1W����+���Cr-	@�H�Y�1�F�F�@c���|B=�C4��\�TCh�����������9���<�?�~�H���b��rdO�
Vc�	3$���i�h�N�^���j��#i5�P0�L��;|�������;�=�O���������^�{�\lm�D�S�oHj��/m4rN� �K�-��� �Fx��^\��z/���b�;���g�b��yw+�J	e{%9��9����:b'�FQZ������iC�5���^�2tu&��r������{�����.���$z=����V3c�iz>���-�����\�YMw��fJ����u�����[��s�<��e�FlK�!����6�5�hlFs�|�������tl2y�v<%�-:^0���$o�����8=t(�G����
N�s�x'�r�'f9��D���x��p�A'�9q�{�O�Eo��g%:|����_���'�8q��u'�':�X���N�:�r�9'v:���r����u:�)I������i�P7/pbGw�?��<'�4 [�k���w{���*����s���+v2_�;��d�+���~K_Bu�h$�X�=��iBlB�)�x<��Xdt������|Ta�vc�O������^�6A;��&���a�\�2�[� �k�`���I05��e�����7��hA ]n�3v�j(����HfN&Q|���?]��-��a��,P���N$U����(�|5�����������K�y��������yb�f�������V,�.��x�����5�t�m��m})a�6�9��^�0P��(���S�
��rBow��[�:�B�$�=��������@CW*�u6�7�[�&�
\���OSfz���f�<���e��Lz�
�
���'�$��6�+�i�r��L�]�w����{�Xw��l�nM�-��+�2)s����}��ch���T��
�=��'��k��%��7�]q3�I���}�6�1u�H}�IWl����>����h�z
�	 �!�h>p�T�D�Ys�~�ko�8��F�@
��Qf*��� tH��
�@E�:A�T�AU�~sQ��W��8H������Fd�G9o{�lb��C���.��P��@�
<D��t���=L�����V�aL�s�^
�����&����y��2(-���0�C����M���&����2��Y��&��SI��0i����� ��?�^����QB�W3�{���u����7m����__�v#��SOnyr���[���n�:�|������4�:�er9�����}r��]��bC�����'O^������/>�<qHyz����}A[�Y�]���0R3���l}l<PnQ�a4�S�a3���y���jus^�;�H���gD����6����[�5-�>��[8��e�~m�K�\t��G~���I
���~���O?������GV�#A������L@�A'`-���Q��W1[���;<����
h���{��$�"��E=��^��_�I���q3�$��!<������UO��f�X ���q�=��W�957���Q�v�_��@@
����:�V�!���e^��Y���G�z1�`������ ~Zm]~�&���B�x��~�-W��`a��K��x^�ja�/�}�'k�`<�_�n����B[/Xn��k���<f��������f�����(�����n�����yx�w��a	����s`
F`+N�jX�dP
3`�UL�a-<�A�e�8�/�.������z���#�_�?C�/����������eqw_�����K]��#0�Z�3������]���l�R��6�cl,��^��@�a��pe��	��C���|S��cJo��u�y��k�]3lh����A�9��e{��L��iL��KM��X��H!GT0R��^Q�J%Rmin�X��]��S"#�X+*��BeK��H�U���d�*bm/pD�kEe�U�r�R��D�8F�CH�r�X�qJy�$*��������7ku*[kp�R����5�Ti�%x����Hqn�����F�H�����������P�K���$j���Vt�:�BzKj��PyeI���	���Q�R����K���Kq�*:�[s:�j7����P'���R������f����AE�)�be�}���9%3���D��\����SvyHTh�I��#���WBj��k���B�Vp|�G-���47%1�i�m�7N�D���j04��DDB�
���_]�R��Sd6'�_�X��V*�7(��UH�Bz���G��	�74(�h�Q5���jX�.�����X^�h�0���<_X!"*��c�P1��������S6��Y��c���9���Vi������#���.��l����F+*�wL�Q��F����Be�]�MZ��}�u���P��Y,���<�O�TI��=��4Nss�R_�&V*r�X����+i��+�Jj#
F�k���I��U����X%s&Tj]���h"�����m_�%����*/�����;[���=~�b��>�R!�K�+�f*���N#3�J�G��
����a��$�2���GX����e���)��$I Tv���*6R�+�F�����+	V(�I��bP����
�UX�Na�&�I@U�5B�DtS+:�b���$����)�����nn��T02���	{%7�P(��X��:U���(�+*�W����H��S5z�R�!����"�*�������T����ZM���KY�9
x�&�4Te*A���r��vO��*��n�����&4���$CP�T��\�Y�nh)X+�&1�������n���U&���fiB���l|������P�eG���0�U���V�&L�|� 6M��M 1:2*���M�����%T�
T��P9���M�4z�+2@���4�������t�0���DfJ��
$���F���`z�.k�`ZiUer
-�d�l 8���*h7-�^E=�r�jm$F��������]	�F���������S*��C��������S��-��n�D�S
�W���������^�����HPid+�AI�f�RR�Q*<��	8��Yi��v��QbtHA��Vz$�"���j6�QW*���i6}���H��
���rj���;����d�������hA�";26F��xU�0�8JL���0���vh"��M�a*�IT��;���M�4����$\�7��B���&*
�)�"��P�����wQQh���(,g���(���;�eTB��[�(���0��B�(���(��}`U���
+�S�[��"�K�%��DS+��t�%S�,c�d+����t���R�R�����
\67���x������sS�iGR#a��>0A�@l%(�����X82��2X����np�����P���_�����c��)y;vt��.�.Lw���]���|�������sA������v����k��:K�~q���o����(��?��?�<�y���;�C��A���3q����t�7_@�?������P�%���\w��$��^GV�������C~��
~p�w�p��v������+j�ool'����vsA��/�o�������o�>���w��Vv��n\�2*/#�2��=�=g����Z�P���B��
�"Z^R^":^:���#����"vl?���m�6"o��m���Q�6f�Cq�z<����_�p�����[�.���D~�h|��4�!����5����yh�C��`��e9.[:��
���l��;G�����Y��wV�~��!��H(�]�v���:��=%8�m)0W�HVPd��$��M����H�ly\�+'���k�r�����	���`���`�{WO��� �l����@�@@���5�"���<~?�_����<��gyr.�8P?����k['N�������2�
MU�I�NP�r��iR�b���V����W��Q}���	�J�O�L��P��j�qB�b��j�Q�hC��.�Z0Q��/Uk��|	�VC_�*}�!�C_�]�E0m�hCC�!���hC4
��/E��Q_����:�_u�!1D4Z�F1�&�sV��h5
endstream
endobj
194 0 obj
   8671
endobj
195 0 obj
<< /Length 196 0 R
   /Filter /FlateDecode
>>
stream
x�]����0��~
7��
��dY����(d�f)c��s����5�H9��M���4�0��i+����0Q���b��~���B�-���q����p����t���i��s��E��Z�]>}��B_�����#�-����0�4���](�
�e�U4����>_H6��y���V��������~�Tu}R�#]�9P����k[/��zA)���{�,k���pF{���h/�y�~a}�>y����*�t��Sm��e�#���]��e�yE��5���t������oQ��{����u�`������Yh��hkF�M������e8���Vle x����<�yz0���!o�y;�����i�<5�j�k����`������A�������Gc]�xT=�K�:���c���9o��3�R���O����?8������W�X
endstream
endobj
196 0 obj
   416
endobj
197 0 obj
<< /Type /FontDescriptor
   /FontName /HPWEWC+CairoFont-0-0
   /Flags 32
   /FontBBox [ -15 -207 853 724 ]
   /ItalicAngle 0
   /Ascent 905
   /Descent -211
   /CapHeight 724
   /StemV 80
   /StemH 80
   /FontFile2 193 0 R
>>
endobj
7 0 obj
<< /Type /Font
   /Subtype /TrueType
   /BaseFont /HPWEWC+CairoFont-0-0
   /FirstChar 32
   /LastChar 121
   /FontDescriptor 197 0 R
   /Encoding /WinAnsiEncoding
   /Widths [ 277.832031 0 0 0 0 889.160156 0 0 0 0 0 0 0 333.007812 277.832031 0 556.152344 556.152344 556.152344 556.152344 556.152344 556.152344 556.152344 556.152344 556.152344 556.152344 0 0 0 0 0 0 0 0 0 0 722.167969 0 0 0 0 277.832031 0 0 0 0 0 0 666.992188 777.832031 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 556.152344 556.152344 500 556.152344 556.152344 277.832031 556.152344 556.152344 222.167969 0 500 222.167969 833.007812 556.152344 556.152344 556.152344 0 333.007812 500 277.832031 556.152344 500 722.167969 500 500 ]
    /ToUnicode 195 0 R
>>
endobj
192 0 obj
<< /Type /ObjStm
   /Length 200 0 R
   /N 30
   /First 246
   /Filter /FlateDecode
>>
stream
x���Mk�J���|��)7�`������h��BH�+]G���e$�r���FN
ef70��t�'!���Bb���AA��H�(��Y��2BpV+�!D��XC��������F��!�+��1%�CT�d��
d��
Z�Dk��B�3Y�Q�#YX�x�N�rp��49���9o����D�By���	FAy�#yqQ������sO����O���_�/}'&MO����4�!iX��[op1
��&
Y���%��j\��g�N����u���]�-��\�-��g���}��������i&j�K����r��#����86������|�>l���0v����������g���8��������������������!~��������4S����k�r�>��wZ��b#��[�n�����������������r����b�^��N_��i�m�N�yy�����g��K�zu�L��������q9�����~.�����=�y/�y
�Hgc�^,�A-P�E6�T��-r����\[/�}�\v�\��u�r�i����\����Q��������bnh�k}!�gs�^�k�=�J_D!���z4����+yd�����7zc�����k�l	yX�������(�<�P�V��VR���BE]���%]!�+T�t`A���<�P�X@dA���<�P�Y@h%i!O-T����%m!�-T�Lp����<��"�`Z���.������]�%wq��V�7Z��+-W����5�j�&��\��V�^\�^�-�_����5���+���y}qM}I}���8�/��/i�/)�����}7w��k�a�7������o��
endstream
endobj
200 0 obj
   825
endobj
201 0 obj
<< /Type /XRef
   /Length 615
   /Filter /FlateDecode
   /Size 202
   /W [1 3 2]
   /Root 199 0 R
   /Info 198 0 R
>>
stream
x�5�KHTa�������c����TN����#��N^��bF�(2��h�E"K+F0�RH��@k%e#A���-\�D�2�@H���k�����3�3���uDzEHG%�"�">0t�>ND�?/a6�����*e�4w�������^����J�=�o`�������V���]���N��sO�����U�l�i'/�����'�������]D53�>��	�Y�Z�A�,������[�Z�J���K����ge�j7�a%�}�>��OF���:v��18VL��{��r�:��4gE���������V.T'������?���^`e�:���`�������gR����O��4��n����LR'�K�F������V.Rg�3�p<J_�������n ������ }�p�f+��-���������c��E�V��[]I���V���:n�X�\�����}������l����'+��rh~����oaU��V.VOn�'�[�J�V�YP�qK��������kCV.U�d�f'X����w`9l�2�jv��k���rw��[�\�^�+��`G;����d��+�8L��F�k�� �o���8���������nJ��7S?�\���
endstream
endobj
startxref
327004
%%EOF
q702.sqlapplication/sql; name=q702.sqlDownload
run-mdam.shapplication/x-shellscript; name=run-mdam.shDownload
In reply to: Peter Geoghegan (#23)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Mon, Sep 16, 2024 at 3:13 PM Peter Geoghegan <pg@bowt.ie> wrote:

I agree with your approach, but I'm concerned about it causing
confusion inside _bt_parallel_done. And so I attach a v2 revision of
your bug fix. v2 adds a check that nails that down, too.

Pushed this just now.

Thanks
--
Peter Geoghegan

In reply to: Peter Geoghegan (#18)
4 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Sep 4, 2024 at 12:52 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v6, which finally does something sensible in btcostestimate.

Attached is v7, which is just to fix bitrot, and keep CFBot happy. No
real changes here.

I will work through Tomas' recent feedback in the next few days.

--
Peter Geoghegan

Attachments:

v7-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v7-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From b28a6760b56ebc5b1b5bbecc155a4d5abe177618 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v7 1/4] Show index search count in EXPLAIN ANALYZE.

Also stop counting the case where nbtree detects contradictory quals as
a distinct index search (do so neither in EXPLAIN ANALYZE nor in the
pg_stat_*_indexes.idx_scan stats).

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |  3 +
 src/backend/access/brin/brin.c                |  1 +
 src/backend/access/gin/ginscan.c              |  1 +
 src/backend/access/gist/gistget.c             |  2 +
 src/backend/access/hash/hashsearch.c          |  1 +
 src/backend/access/index/genam.c              |  1 +
 src/backend/access/nbtree/nbtree.c            | 11 ++++
 src/backend/access/nbtree/nbtsearch.c         |  9 ++-
 src/backend/access/spgist/spgscan.c           |  1 +
 src/backend/commands/explain.c                | 38 +++++++++++++
 doc/src/sgml/bloom.sgml                       |  6 +-
 doc/src/sgml/monitoring.sgml                  | 12 +++-
 doc/src/sgml/perform.sgml                     |  8 +++
 doc/src/sgml/ref/explain.sgml                 |  3 +-
 doc/src/sgml/rules.sgml                       |  1 +
 src/test/regress/expected/brin_multi.out      | 27 ++++++---
 src/test/regress/expected/memoize.out         | 50 +++++++++++-----
 src/test/regress/expected/partition_prune.out | 57 ++++++++++++++-----
 src/test/regress/expected/select.out          |  3 +-
 src/test/regress/sql/memoize.sql              |  6 +-
 src/test/regress/sql/partition_prune.sql      |  4 ++
 21 files changed, 198 insertions(+), 47 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 521043304..b992d4080 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -130,6 +130,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 60853a0f6..879d5589d 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -582,6 +582,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index f2fd62afb..5e423e155 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index b35b8a975..36f1435cb 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index 0d99d6abc..927ba1039 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 4ec43e3c0..466d766b0 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -116,6 +116,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 56e502c4f..b413433d9 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -70,6 +70,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* instrumentation */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -550,6 +551,7 @@ btinitparallelscan(void *target)
 	SpinLockInit(&bt_target->btps_mutex);
 	bt_target->btps_scanPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -575,6 +577,7 @@ btparallelrescan(IndexScanDesc scan)
 	SpinLockAcquire(&btscan->btps_mutex);
 	btscan->btps_scanPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -683,6 +686,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			*pageno = btscan->btps_scanPage;
 			exit_loop = true;
@@ -764,6 +772,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -797,6 +807,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 	{
 		btscan->btps_scanPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 2551df8a6..4b91a192e 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -896,8 +896,6 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 
 	Assert(!BTScanPosIsValid(so->currPos));
 
-	pgstat_count_index_scan(rel);
-
 	/*
 	 * Examine the scan keys and eliminate any redundant keys; also mark the
 	 * keys that must be matched to continue the scan.
@@ -960,6 +958,13 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		_bt_start_array_keys(scan, dir);
 	}
 
+	/*
+	 * We've established that we'll either call _bt_search or _bt_endpoint.
+	 * Count this as a primitive index scan/index search.
+	 */
+	pgstat_count_index_scan(rel);
+	scan->nsearches++;
+
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
 	 *
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 301786185..be668abf2 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index aaec43989..6f6d5a8c2 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -89,6 +90,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -1989,6 +1991,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			if (es->analyze)
+				show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2002,6 +2006,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			if (es->analyze)
+				show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2018,6 +2024,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			if (es->analyze)
+				show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2528,6 +2536,36 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc && scanDesc->nsearches > 0)
+		ExplainPropertyUInteger("Index Searches", NULL,
+								scanDesc->nsearches, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 19f2b172c..92b13f539 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -170,9 +170,10 @@ CREATE INDEX
    Heap Blocks: exact=28
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..1792.00 rows=2 width=0) (actual time=0.356..0.356 rows=29 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 0.408 ms
-(8 rows)
+(9 rows)
 </programlisting>
   </para>
 
@@ -202,11 +203,12 @@ CREATE INDEX
    -&gt;  BitmapAnd  (cost=24.34..24.34 rows=2 width=0) (actual time=0.027..0.027 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..12.04 rows=500 width=0) (actual time=0.026..0.026 rows=0 loops=1)
                Index Cond: (i5 = 123451)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
-(9 rows)
+(10 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 933de6fe0..8a314b2dc 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4150,12 +4150,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index ff689b652..1f2172960 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -702,8 +702,10 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Heap Blocks: exact=10
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 1
  Planning Time: 0.485 ms
  Execution Time: 0.073 ms
 </screen>
@@ -754,6 +756,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Heap Blocks: exact=90
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
  Planning Time: 0.187 ms
  Execution Time: 3.036 ms
 </screen>
@@ -819,6 +822,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -848,9 +852,11 @@ EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique
          Buffers: shared hit=4 read=3
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared hit=2
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
                Buffers: shared hit=2 read=3
  Planning:
    Buffers: shared hit=3
@@ -883,6 +889,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Heap Blocks: exact=90
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1017,6 +1024,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
  Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
  Planning Time: 0.077 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a854..e042638b7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -502,9 +502,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    Batches: 1  Memory Usage: 24kB
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(7 rows)
+(8 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index ae9ce9d8e..c24d56007 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 9ee09fe2f..1448179fb 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -312,7 +325,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(10 rows)
+               Index Searches: N
+(11 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -329,7 +343,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(10 rows)
+               Index Searches: N
+(11 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -349,6 +364,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -356,9 +372,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -366,8 +384,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -379,6 +398,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -387,11 +407,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7a03b4e36..18ea272b6 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2692,12 +2696,13 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+(53 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off)
@@ -2713,6 +2718,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
@@ -2741,7 +2747,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(38 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off)
@@ -2757,6 +2763,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
@@ -2787,7 +2794,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(40 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2865,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2885,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,8 +2974,10 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
@@ -2971,7 +2986,7 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
                Index Cond: (col1 < tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+(17 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2984,6 +2999,7 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
@@ -2992,7 +3008,7 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+(16 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3043,20 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 5
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 4
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+(18 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3050,15 +3069,17 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 3
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+(17 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3122,7 +3143,8 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
                Index Cond: (col1 > tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(16 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3482,12 +3504,14 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(15);
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3527,10 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(25);
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3578,16 @@ explain (analyze, costs off, summary off, timing off) select * from ma_test wher
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(17 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4157,17 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(18 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 33a6dceb0..b7cf35b9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index 2eaeb1477..9afe205e0 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 442428d93..085e746af 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

v7-0002-Normalize-nbtree-truncated-high-key-array-behavio.patchapplication/octet-stream; name=v7-0002-Normalize-nbtree-truncated-high-key-array-behavio.patchDownload
From d41e016a6bdf38c7bad9a59126421d5adb19d7b7 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Thu, 8 Aug 2024 13:51:18 -0400
Subject: [PATCH v7 2/4] Normalize nbtree truncated high key array behavior.

Commit 5bf748b8 taught nbtree ScalarArrayOp array processing to decide
when and how to start the next primitive index scan based on physical
index characteristics.  This included rules for deciding whether to
start a new primitive index scan (or whether to move onto the right
sibling leaf page instead) whenever the scan encounters a leaf high key
with truncated lower-order columns whose omitted/-inf values are covered
by one or more arrays.

Prior to this commit, nbtree would treat a truncated column as
satisfying a scan key that marked required in the current scan
direction.  It would just give up and start a new primitive index scan
in cases involving inequalities required in the opposite direction only
(in practice this meant > and >= strategy scan keys, since only forward
scans consider the page high key like this).

Bring > and >= strategy scan keys in line with other required scan key
types: have nbtree persist with its current primitive index scan
regardless of the operator strategy in use.  This requires scheduling
and then performing an explicit check of the next page's high key (if
any) at the point that _bt_readpage is next called.

Although this could be considered a stand alone piece of work, it's
mostly intended as preparation for an upcoming patch that adds skip scan
optimizations to nbtree.  Without this work there are cases where the
scan's skip arrays trigger an excessive number of primitive index scans
due to most high keys having a truncated attribute that was previously
treated as not satisfying a required > or >= strategy scan key.

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=9A_UtM7HzUThSkQ+BcrQsQZuNhWOvQWK06PRkEp=SKQ@mail.gmail.com
---
 src/include/access/nbtree.h           |   3 +
 src/backend/access/nbtree/nbtree.c    |   4 +
 src/backend/access/nbtree/nbtsearch.c |  22 +++++
 src/backend/access/nbtree/nbtutils.c  | 119 ++++++++++++++------------
 4 files changed, 95 insertions(+), 53 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index d64300fb9..d709fe08d 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1048,6 +1048,7 @@ typedef struct BTScanOpaqueData
 	int			numArrayKeys;	/* number of equality-type array keys */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Last array advancement matched -inf attr? */
+	bool		oppoDirCheck;	/* check opposite dir scan keys? */
 	BTArrayKeyInfo *arrayKeys;	/* info about each equality-type array key */
 	FmgrInfo   *orderProcs;		/* ORDER procs for required equality keys */
 	MemoryContext arrayContext; /* scan-lifespan context for array data */
@@ -1289,6 +1290,8 @@ extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir);
 extern void _bt_preprocess_keys(IndexScanDesc scan);
 extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 						  IndexTuple tuple, int tupnatts);
+extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+								  IndexTuple finaltup);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index b413433d9..f166c7549 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -333,6 +333,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 
 	so->needPrimScan = false;
 	so->scanBehind = false;
+	so->oppoDirCheck = false;
 	so->arrayKeys = NULL;
 	so->orderProcs = NULL;
 	so->arrayContext = NULL;
@@ -376,6 +377,7 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 	so->markItemIndex = -1;
 	so->needPrimScan = false;
 	so->scanBehind = false;
+	so->oppoDirCheck = false;
 	BTScanPosUnpinIfPinned(so->markPos);
 	BTScanPosInvalidate(so->markPos);
 
@@ -621,6 +623,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 		 */
 		so->needPrimScan = false;
 		so->scanBehind = false;
+		so->oppoDirCheck = false;
 	}
 	else
 	{
@@ -679,6 +682,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			 */
 			so->needPrimScan = true;
 			so->scanBehind = false;
+			so->oppoDirCheck = false;
 		}
 		else if (btscan->btps_pageStatus != BTPARALLEL_ADVANCING)
 		{
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 4b91a192e..e5f941e0a 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1704,6 +1704,28 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			ItemId		iid = PageGetItemId(page, P_HIKEY);
 
 			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+			if (unlikely(so->oppoDirCheck))
+			{
+				/*
+				 * Last _bt_readpage call scheduled precheck of finaltup for
+				 * required scan keys up to and including a > or >= scan key
+				 * (necessary because > and >= are only generally considered
+				 * required when scanning backwards)
+				 */
+				Assert(so->scanBehind);
+				so->oppoDirCheck = false;
+				if (!_bt_oppodir_checkkeys(scan, dir, pstate.finaltup))
+				{
+					/*
+					 * Back out of continuing with this leaf page -- schedule
+					 * another primitive index scan after all
+					 */
+					so->currPos.moreRight = false;
+					so->needPrimScan = true;
+					return false;
+				}
+			}
 		}
 
 		/* load items[] in ascending order */
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index c22ccec78..98688a3d6 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -1362,7 +1362,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 			curArrayKey->cur_elem = 0;
 		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
 	}
-	so->scanBehind = false;
+	so->scanBehind = so->oppoDirCheck = false;	/* reset */
 }
 
 /*
@@ -1671,8 +1671,7 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
 
 	Assert(so->numArrayKeys);
 
-	/* scanBehind flag doesn't persist across primitive index scans - reset */
-	so->scanBehind = false;
+	so->scanBehind = so->oppoDirCheck = false;	/* reset */
 
 	/*
 	 * Array keys are advanced within _bt_checkkeys when the scan reaches the
@@ -1808,7 +1807,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 
-		so->scanBehind = false; /* reset */
+		so->scanBehind = so->oppoDirCheck = false;	/* reset */
 
 		/*
 		 * Required scan key wasn't satisfied, so required arrays will have to
@@ -2293,19 +2292,27 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	if (so->scanBehind && has_required_opposite_direction_only)
 	{
 		/*
-		 * However, we avoid this behavior whenever the scan involves a scan
+		 * However, we do things differently whenever the scan involves a scan
 		 * key required in the opposite direction to the scan only, along with
 		 * a finaltup with at least one truncated attribute that's associated
 		 * with a scan key marked required (required in either direction).
 		 *
 		 * _bt_check_compare simply won't stop the scan for a scan key that's
 		 * marked required in the opposite scan direction only.  That leaves
-		 * us without any reliable way of reconsidering any opposite-direction
+		 * us without an automatic way of reconsidering any opposite-direction
 		 * inequalities if it turns out that starting a new primitive index
 		 * scan will allow _bt_first to skip ahead by a great many leaf pages
 		 * (see next section for details of how that works).
+		 *
+		 * We deal with this by explicitly scheduling a finaltup recheck for
+		 * the next page -- we'll call _bt_oppodir_checkkeys for the next
+		 * page's finaltup instead.  You can think of this as a way of dealing
+		 * with this page's finaltup being truncated by checking the next
+		 * page's finaltup instead.  And you can think of the oppoDirCheck
+		 * recheck handling within _bt_readpage as complementing the similar
+		 * scanBehind recheck made from within _bt_checkkeys.
 		 */
-		goto new_prim_scan;
+		so->oppoDirCheck = true;	/* schedule next page's finaltup recheck */
 	}
 
 	/*
@@ -2343,54 +2350,16 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * also before the _bt_first-wise start of tuples for our new qual.  That
 	 * at least suggests many more skippable pages beyond the current page.
 	 */
-	if (has_required_opposite_direction_only && pstate->finaltup &&
-		(all_required_satisfied || oppodir_inequality_sktrig))
+	else if (has_required_opposite_direction_only && pstate->finaltup &&
+			 (all_required_satisfied || oppodir_inequality_sktrig) &&
+			 unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup)))
 	{
-		int			nfinaltupatts = BTreeTupleGetNAtts(pstate->finaltup, rel);
-		ScanDirection flipped;
-		bool		continuescanflip;
-		int			opsktrig;
-
 		/*
-		 * We're checking finaltup (which is usually not caller's tuple), so
-		 * cannot reuse work from caller's earlier _bt_check_compare call.
-		 *
-		 * Flip the scan direction when calling _bt_check_compare this time,
-		 * so that it will set continuescanflip=false when it encounters an
-		 * inequality required in the opposite scan direction.
+		 * Make sure that any non-required arrays are set to the first array
+		 * element for the current scan direction
 		 */
-		Assert(!so->scanBehind);
-		opsktrig = 0;
-		flipped = -dir;
-		_bt_check_compare(scan, flipped,
-						  pstate->finaltup, nfinaltupatts, tupdesc,
-						  false, false, false,
-						  &continuescanflip, &opsktrig);
-
-		/*
-		 * Only start a new primitive index scan when finaltup has a required
-		 * unsatisfied inequality (unsatisfied in the opposite direction)
-		 */
-		Assert(all_required_satisfied != oppodir_inequality_sktrig);
-		if (unlikely(!continuescanflip &&
-					 so->keyData[opsktrig].sk_strategy != BTEqualStrategyNumber))
-		{
-			/*
-			 * It's possible for the same inequality to be unsatisfied by both
-			 * caller's tuple (in scan's direction) and finaltup (in the
-			 * opposite direction) due to _bt_check_compare's behavior with
-			 * NULLs
-			 */
-			Assert(opsktrig >= sktrig); /* not opsktrig > sktrig due to NULLs */
-
-			/*
-			 * Make sure that any non-required arrays are set to the first
-			 * array element for the current scan direction
-			 */
-			_bt_rewind_nonrequired_arrays(scan, dir);
-
-			goto new_prim_scan;
-		}
+		_bt_rewind_nonrequired_arrays(scan, dir);
+		goto new_prim_scan;
 	}
 
 	/*
@@ -3522,7 +3491,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 *
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
-		Assert(!so->scanBehind && !pstate->prechecked && !pstate->firstmatch);
+		Assert(!so->scanBehind && !so->oppoDirCheck);
+		Assert(!pstate->prechecked && !pstate->firstmatch);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
@@ -3634,6 +3604,49 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 								  ikey, true);
 }
 
+/*
+ * Test whether an indextuple satisfies inequalities required in the opposite
+ * direction only (and lower-order equalities required in either direction).
+ *
+ * scan: index scan descriptor (containing a search-type scankey)
+ * dir: current scan direction (flipped by us to get opposite direction)
+ * finaltup: final index tuple on the page
+ *
+ * Caller's finaltup tuple is the page high key (for forwards scans), or the
+ * first non-pivot tuple (for backwards scans).  Caller during scans with
+ * required array keys.
+ *
+ * Return true if finatup satisfies keys, false if not.  If the tuple fails to
+ * pass the qual, then caller is should start another primitive index scan;
+ * _bt_first can efficiently relocate the scan to a far later leaf page.
+ *
+ * Note: we focus on required-in-opposite-direction scan keys (e.g. for a
+ * required > or >= key, assuming a forwards scan) because _bt_checkkeys() can
+ * always deal with required-in-current-direction scan keys on its own.
+ */
+bool
+_bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+					  IndexTuple finaltup)
+{
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	int			nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+	bool		continuescan;
+	ScanDirection flipped = -dir;
+	int			ikey = 0;
+
+	Assert(so->numArrayKeys);
+
+	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
+					  false, false, false, &continuescan, &ikey);
+
+	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
+		return false;
+
+	return true;
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
-- 
2.45.2

v7-0003-Refactor-handling-of-nbtree-array-redundancies.patchapplication/octet-stream; name=v7-0003-Refactor-handling-of-nbtree-array-redundancies.patchDownload
From 002753a55696ec3f51537b8ca2b6f49c5f01086e Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Thu, 8 Aug 2024 15:41:18 -0400
Subject: [PATCH v7 3/4] Refactor handling of nbtree array redundancies.

Rather than allocating memory for so.keyData[] at the start of each
btrescan, lazily allocate space later on, in _bt_preprocess_keys.  We
now allocate so.keyData[] after _bt_preprocess_array_keys is done
performing initial array related preprocessing.

An immediate benefit of this approach is that _bt_preprocess_array_keys
no longer needs to explicitly mark redundant array scan keys.  Other
code (_bt_preprocess_keys and its other subsidiary routines) no longer
have to interpret the scan key entries as redundant.  Redundant array
scan keys simply never appear in the _bt_preprocess_keys input array
(_bt_preprocess_array_keys removes them up front).

This refactoring is also preparation for an upcoming patch that will add
skip scan optimizations to nbtree.  _bt_preprocess_array_keys will be
taught to add new skip array scan keys to the _bt_preprocess_keys input
array (i.e. to arrayKeyData), so doing things this way avoids uselessly
palloc'ing so.keyData[], only to have to repalloc (to enlarge the array)
almost immediately afterwards.  This scheme allows _bt_preprocess_keys
to output a so.keyData[] scan key array that can be larger than the
original scan.keyData[] input array, due to the addition of skip array
scan keys within _bt_preprocess_array_keys.

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=9A_UtM7HzUThSkQ+BcrQsQZuNhWOvQWK06PRkEp=SKQ@mail.gmail.com
---
 src/backend/access/nbtree/nbtree.c   |  10 +-
 src/backend/access/nbtree/nbtutils.c | 156 +++++++++++++--------------
 2 files changed, 82 insertions(+), 84 deletions(-)

diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index f166c7549..4ecf883d3 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -326,11 +326,8 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	so = (BTScanOpaque) palloc(sizeof(BTScanOpaqueData));
 	BTScanPosInvalidate(so->currPos);
 	BTScanPosInvalidate(so->markPos);
-	if (scan->numberOfKeys > 0)
-		so->keyData = (ScanKey) palloc(scan->numberOfKeys * sizeof(ScanKeyData));
-	else
-		so->keyData = NULL;
 
+	so->keyData = NULL;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->oppoDirCheck = false;
@@ -410,6 +407,11 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 		memcpy(scan->keyData, scankey, scan->numberOfKeys * sizeof(ScanKeyData));
 	so->numberOfKeys = 0;		/* until _bt_preprocess_keys sets it */
 	so->numArrayKeys = 0;		/* ditto */
+
+	/* Release private storage allocated in previous btrescan, if any */
+	if (so->keyData != NULL)
+		pfree(so->keyData);
+	so->keyData = NULL;
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 98688a3d6..d1423bd85 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -62,7 +62,7 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
-static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan);
+static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
@@ -251,9 +251,6 @@ _bt_freestack(BTStack stack)
  * It is convenient for _bt_preprocess_keys caller to have to deal with no
  * more than one equality strategy array scan key per index attribute.  We'll
  * always be able to set things up that way when complete opfamilies are used.
- * Eliminated array scan keys can be recognized as those that have had their
- * sk_strategy field set to InvalidStrategy here by us.  Caller should avoid
- * including these in the scan's so->keyData[] output array.
  *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
@@ -261,18 +258,25 @@ _bt_freestack(BTStack stack)
  * preprocessing steps are complete.  This will convert the scan key offset
  * references into references to the scan's so->keyData[] output scan keys.
  *
+ * Caller must pass *numberOfKeys to give us a way to change the number of
+ * input scan keys (our output is caller's input).  The returned array can be
+ * smaller than scan->keyData[] when we eliminated a redundant array scan key
+ * (redundant with some other array scan key, for the same attribute).  Caller
+ * uses this to allocate so->keyData[] for the current btrescan.
+ *
  * Note: the reason we need to return a temp scan key array, rather than just
  * scribbling on scan->keyData, is that callers are permitted to call btrescan
  * without supplying a new set of scankey data.
  */
 static ScanKey
-_bt_preprocess_array_keys(IndexScanDesc scan)
+_bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
+	int			numArrayKeyData = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
-	int			numArrayKeys;
+	int			numArrayKeys,
+				output_ikey = 0;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -280,11 +284,11 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
+	Assert(scan->numberOfKeys);
 
 	/* Quick check to see if there are any array keys */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
 		cur = &scan->keyData[i];
 		if (cur->sk_flags & SK_SEARCHARRAY)
@@ -317,19 +321,18 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
-	/* Create modifiable copy of scan->keyData in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
-	memcpy(arrayKeyData, scan->keyData, numberOfKeys * sizeof(ScanKeyData));
+	/* Create output scan keys in the workspace context */
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
 	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -345,14 +348,20 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		int			num_nonnulls;
 		int			j;
 
-		cur = &arrayKeyData[i];
+		/*
+		 * Copy input scan key into temp arrayKeyData scan key array
+		 */
+		cur = &arrayKeyData[output_ikey];
+		*cur = scan->keyData[input_ikey];
+
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
+		{
+			output_ikey++;		/* keep this non-array scan key */
 			continue;
+		}
 
 		/*
-		 * First, deconstruct the array into elements.  Anything allocated
-		 * here (including a possibly detoasted array value) is in the
-		 * workspace context.
+		 * Deconstruct the array into elements
 		 */
 		arrayval = DatumGetArrayTypeP(cur->sk_argument);
 		/* We could cache this data, but not clear it's worth it */
@@ -406,6 +415,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
+				output_ikey++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -416,6 +426,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
+				output_ikey++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -432,7 +443,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[i], &sortprocp);
+							&so->orderProcs[output_ikey], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -476,11 +487,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 					break;
 				}
 
-				/*
-				 * Indicate to _bt_preprocess_keys caller that it must ignore
-				 * this scan key
-				 */
-				cur->sk_strategy = InvalidStrategy;
+				/* Throw away this scan key/array */
 				continue;
 			}
 
@@ -511,12 +518,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = i;
+		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
 		numArrayKeys++;
+		output_ikey++;			/* keep this scan key/array */
 	}
 
+	/* Set final number of arrayKeyData[] keys, array keys */
+	*numberOfKeys = output_ikey;
 	so->numArrayKeys = numArrayKeys;
 
 	MemoryContextSwitchTo(oldContext);
@@ -2429,10 +2439,12 @@ end_toplevel_scan:
 /*
  *	_bt_preprocess_keys() -- Preprocess scan keys
  *
+ * The first call here (per btrescan) allocates so->keyData[].
  * The given search-type keys (taken from scan->keyData[])
  * are copied to so->keyData[] with possible transformation.
  * scan->numberOfKeys is the number of input keys, so->numberOfKeys gets
- * the number of output keys (possibly less, never greater).
+ * the number of output keys.  Calling here a second or subsequent time
+ * (during the same btrescan) is a no-op.
  *
  * The output keys are marked with additional sk_flags bits beyond the
  * system-standard bits supplied by the caller.  The DESC and NULLS_FIRST
@@ -2519,9 +2531,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	int16	   *indoption = scan->indexRelation->rd_indoption;
 	int			new_numberOfKeys;
 	int			numberOfEqualCols;
-	ScanKey		inkeys;
-	ScanKey		outkeys;
-	ScanKey		cur;
+	ScanKey		inputsk;
 	BTScanKeyPreproc xform[BTMaxStrategyNumber];
 	bool		test_result;
 	int			i,
@@ -2553,7 +2563,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		return;					/* done if qual-less scan */
 
 	/* If any keys are SK_SEARCHARRAY type, set up array-key info */
-	arrayKeyData = _bt_preprocess_array_keys(scan);
+	arrayKeyData = _bt_preprocess_array_keys(scan, &numberOfKeys);
 	if (!so->qual_ok)
 	{
 		/* unmatchable array, so give up */
@@ -2567,32 +2577,36 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	 */
 	if (arrayKeyData)
 	{
-		inkeys = arrayKeyData;
+		inputsk = arrayKeyData;
 
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
 	}
 	else
-		inkeys = scan->keyData;
+		inputsk = scan->keyData;
+
+	/*
+	 * Now that we have an estimate of the number of output scan keys,
+	 * allocate space for them
+	 */
+	so->keyData = palloc(sizeof(ScanKeyData) * numberOfKeys);
 
-	outkeys = so->keyData;
-	cur = &inkeys[0];
 	/* we check that input keys are correctly ordered */
-	if (cur->sk_attno < 1)
+	if (inputsk[0].sk_attno < 1)
 		elog(ERROR, "btree index keys must be ordered by attribute");
 
 	/* We can short-circuit most of the work if there's just one key */
 	if (numberOfKeys == 1)
 	{
 		/* Apply indoption to scankey (might change sk_strategy!) */
-		if (!_bt_fix_scankey_strategy(cur, indoption))
+		if (!_bt_fix_scankey_strategy(inputsk, indoption))
 			so->qual_ok = false;
-		memcpy(outkeys, cur, sizeof(ScanKeyData));
+		memcpy(&so->keyData[0], &inputsk[0], sizeof(ScanKeyData));
 		so->numberOfKeys = 1;
 		/* We can mark the qual as required if it's for first index col */
-		if (cur->sk_attno == 1)
-			_bt_mark_scankey_required(outkeys);
+		if (inputsk[0].sk_attno == 1)
+			_bt_mark_scankey_required(&so->keyData[0]);
 		if (arrayKeyData)
 		{
 			/*
@@ -2600,8 +2614,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * (we'll miss out on the single value array transformation, but
 			 * that's not nearly as important when there's only one scan key)
 			 */
-			Assert(cur->sk_flags & SK_SEARCHARRAY);
-			Assert(cur->sk_strategy != BTEqualStrategyNumber ||
+			Assert(so->keyData[0].sk_flags & SK_SEARCHARRAY);
+			Assert(so->keyData[0].sk_strategy != BTEqualStrategyNumber ||
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
@@ -2629,12 +2643,12 @@ _bt_preprocess_keys(IndexScanDesc scan)
 	 * handle after-last-key processing.  Actual exit from the loop is at the
 	 * "break" statement below.
 	 */
-	for (i = 0;; cur++, i++)
+	for (i = 0;; inputsk++, i++)
 	{
 		if (i < numberOfKeys)
 		{
 			/* Apply indoption to scankey (might change sk_strategy!) */
-			if (!_bt_fix_scankey_strategy(cur, indoption))
+			if (!_bt_fix_scankey_strategy(inputsk, indoption))
 			{
 				/* NULL can't be matched, so give up */
 				so->qual_ok = false;
@@ -2646,12 +2660,12 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		 * If we are at the end of the keys for a particular attr, finish up
 		 * processing and emit the cleaned-up keys.
 		 */
-		if (i == numberOfKeys || cur->sk_attno != attno)
+		if (i == numberOfKeys || inputsk->sk_attno != attno)
 		{
 			int			priorNumberOfEqualCols = numberOfEqualCols;
 
 			/* check input keys are correctly ordered */
-			if (i < numberOfKeys && cur->sk_attno < attno)
+			if (i < numberOfKeys && inputsk->sk_attno < attno)
 				elog(ERROR, "btree index keys must be ordered by attribute");
 
 			/*
@@ -2755,7 +2769,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			}
 
 			/*
-			 * Emit the cleaned-up keys into the outkeys[] array, and then
+			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
 			 */
@@ -2763,7 +2777,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			{
 				if (xform[j].skey)
 				{
-					ScanKey		outkey = &outkeys[new_numberOfKeys++];
+					ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
 					memcpy(outkey, xform[j].skey, sizeof(ScanKeyData));
 					if (arrayKeyData)
@@ -2780,19 +2794,19 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				break;
 
 			/* Re-initialize for new attno */
-			attno = cur->sk_attno;
+			attno = inputsk->sk_attno;
 			memset(xform, 0, sizeof(xform));
 		}
 
 		/* check strategy this key's operator corresponds to */
-		j = cur->sk_strategy - 1;
+		j = inputsk->sk_strategy - 1;
 
 		/* if row comparison, push it directly to the output array */
-		if (cur->sk_flags & SK_ROW_HEADER)
+		if (inputsk->sk_flags & SK_ROW_HEADER)
 		{
-			ScanKey		outkey = &outkeys[new_numberOfKeys++];
+			ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
-			memcpy(outkey, cur, sizeof(ScanKeyData));
+			memcpy(outkey, inputsk, sizeof(ScanKeyData));
 			if (arrayKeyData)
 				keyDataMap[new_numberOfKeys - 1] = i;
 			if (numberOfEqualCols == attno - 1)
@@ -2806,21 +2820,10 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			continue;
 		}
 
-		/*
-		 * Does this input scan key require further processing as an array?
-		 */
-		if (cur->sk_strategy == InvalidStrategy)
+		if (inputsk->sk_strategy == BTEqualStrategyNumber &&
+			(inputsk->sk_flags & SK_SEARCHARRAY))
 		{
-			/* _bt_preprocess_array_keys marked this array key redundant */
-			Assert(arrayKeyData);
-			Assert(cur->sk_flags & SK_SEARCHARRAY);
-			continue;
-		}
-
-		if (cur->sk_strategy == BTEqualStrategyNumber &&
-			(cur->sk_flags & SK_SEARCHARRAY))
-		{
-			/* _bt_preprocess_array_keys kept this array key */
+			/* maintain arrayidx for xform[] array */
 			Assert(arrayKeyData);
 			arrayidx++;
 		}
@@ -2832,7 +2835,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		if (xform[j].skey == NULL)
 		{
 			/* nope, so this scan key wins by default (at least for now) */
-			xform[j].skey = cur;
+			xform[j].skey = inputsk;
 			xform[j].ikey = i;
 			xform[j].arrayidx = arrayidx;
 		}
@@ -2850,7 +2853,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/*
 				 * Have to set up array keys
 				 */
-				if ((cur->sk_flags & SK_SEARCHARRAY))
+				if (inputsk->sk_flags & SK_SEARCHARRAY)
 				{
 					array = &so->arrayKeys[arrayidx - 1];
 					orderproc = so->orderProcs + i;
@@ -2878,7 +2881,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				 */
 			}
 
-			if (_bt_compare_scankey_args(scan, cur, cur, xform[j].skey,
+			if (_bt_compare_scankey_args(scan, inputsk, inputsk, xform[j].skey,
 										 array, orderproc, &test_result))
 			{
 				/* Have all we need to determine redundancy */
@@ -2892,7 +2895,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 					if (j != (BTEqualStrategyNumber - 1) ||
 						!(xform[j].skey->sk_flags & SK_SEARCHARRAY))
 					{
-						xform[j].skey = cur;
+						xform[j].skey = inputsk;
 						xform[j].ikey = i;
 						xform[j].arrayidx = arrayidx;
 					}
@@ -2905,7 +2908,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 						 * scan key.  _bt_compare_scankey_args expects us to
 						 * always keep arrays (and discard non-arrays).
 						 */
-						Assert(!(cur->sk_flags & SK_SEARCHARRAY));
+						Assert(!(inputsk->sk_flags & SK_SEARCHARRAY));
 					}
 				}
 				else if (j == (BTEqualStrategyNumber - 1))
@@ -2928,14 +2931,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				 * even with incomplete opfamilies.  _bt_advance_array_keys
 				 * depends on this.
 				 */
-				ScanKey		outkey = &outkeys[new_numberOfKeys++];
+				ScanKey		outkey = &so->keyData[new_numberOfKeys++];
 
 				memcpy(outkey, xform[j].skey, sizeof(ScanKeyData));
 				if (arrayKeyData)
 					keyDataMap[new_numberOfKeys - 1] = xform[j].ikey;
 				if (numberOfEqualCols == attno - 1)
 					_bt_mark_scankey_required(outkey);
-				xform[j].skey = cur;
+				xform[j].skey = inputsk;
 				xform[j].ikey = i;
 				xform[j].arrayidx = arrayidx;
 			}
@@ -3349,13 +3352,6 @@ _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption)
 		return true;
 	}
 
-	if (skey->sk_strategy == InvalidStrategy)
-	{
-		/* Already-eliminated array scan key; don't need to fix anything */
-		Assert(skey->sk_flags & SK_SEARCHARRAY);
-		return true;
-	}
-
 	/* Adjust strategy for DESC, if we didn't already */
 	if ((addflags & SK_BT_DESC) && !(skey->sk_flags & SK_BT_DESC))
 		skey->sk_strategy = BTCommuteStrategyNumber(skey->sk_strategy);
-- 
2.45.2

v7-0004-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v7-0004-Add-skip-scan-to-nbtree.patchDownload
From 336f8e6ac66bf930add19873040634bbe55985fe Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v7 4/4] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
useful in cases where the total number of distinct values in the column
'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.

Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXTPRIOR scan key flag, without directly changing its sk_argument.
The presence of NEXTPRIOR makes the scan interpret the key's sk_argument
as coming immediately after (or coming immediately before) sk_argument
in the key space.  The key value must still come before (or still come
after) any possible greater-than (or less-than) indexable/non-sentinel
value.  Obviously, the scan will never locate any exactly equal tuples.
But attempting to locate a match serves to make the scan locate the true
next value in whatever way it determines is most efficient, without any
need for special cases in high level scan-related code.  In particular,
this design obviates the need for explicit "next key" index probes.

Though it's typical for nbtree preprocessing to cons up skip arrays when
it will allow the scan to apply one or more omitted-from-query leading
key columns when skipping, that's never a requirement.  There are hardly
any limitations around where skip arrays/scan keys may appear relative
to conventional/input scan keys.  This is no less true in the presence
of conventional SAOP array scan keys, which may both roll over and be
rolled over by skip arrays.  For example, a skip array on the column "b"
is generated with quals such as "WHERE a = 42 AND c IN (1, 2, 3)".  As
with any nbtree scan involving arrays, whether or not we actually skip
depends on the physical characteristics of the index during the scan.

The optimizer doesn't use distinct new index paths to represent index
skip scans.  Skipping isn't an either/or question.  It's possible for
individual index scans to conspicuously vary how and when they skip in
order to deal with variation in how leading column values cluster
together over the key space of the index.  A dynamic strategy seems to
work best.  Skipping can be used during nbtree bitmap index scans,
nbtree index scans, and nbtree index-only scans.  Parallel index skip
scan is also supported.

Preprocessing will never cons up a skip array for an index column that
has an equality strategy scan key on input, but will do so for indexed
columns that are only constrained by inequality type input scan keys.
This allows the scan to skip using a range skip array.  These are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".
Such transformations only happen when they enable later preprocessing to
mark the copied-from-input scan key on "b" required to continue the scan
(otherwise, preprocessing directly outputs the >= and <= keys on "a" in
the traditional way, without adding a superseding skip array on "a").

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   27 +-
 src/include/catalog/pg_amproc.dat             |   16 +
 src/include/catalog/pg_proc.dat               |   24 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  109 ++
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  273 ++++
 src/backend/access/nbtree/nbtree.c            |  205 ++-
 src/backend/access/nbtree/nbtsearch.c         |   93 +-
 src/backend/access/nbtree/nbtutils.c          | 1409 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   46 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  368 +++--
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/uuid.c                  |   71 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/btree.sgml                       |   13 +
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   40 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |    6 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   61 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    2 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 34 files changed, 2626 insertions(+), 308 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c51de742e..0826c124f 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -202,7 +202,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index d709fe08d..e1b6b883e 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1031,10 +1033,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard arrays that store elements in memory */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup set to valid routine? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* opclass skip scan support, when use_sksup */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo	order_low;		/* low_compare's ORDER proc */
+	FmgrInfo	order_high;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1124,6 +1138,9 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array, for skip scan */
+#define SK_BT_NEGPOSINF	0x00080000	/* no sk_argument, -inf/+inf key */
+#define SK_BT_NEXTPRIOR	0x00100000	/* sk_argument is next/prior key */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1160,6 +1177,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1170,7 +1191,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5d7fe292b..c0ccdfdfb 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -122,6 +128,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +149,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +170,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +205,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +275,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2513c36fc..d288aced9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2250,6 +2265,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4437,6 +4455,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -9294,6 +9315,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d70e6d37e..5b58739ad 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..d91390fc6
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,109 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * This happens at the point where the scan determines that another primitive
+ * index scan is required.  The next value is used (in combination with at
+ * least one additional lower-order non-skip key, taken from the SQL query) to
+ * relocate the scan, skipping over many irrelevant leaf pages in the process.
+ *
+ * Skip support generally works best with discrete types such as integer,
+ * date, and boolean; types where there is a decent chance that indexes will
+ * contain contiguous values (given a leading attributes using the opclass).
+ * When gaps/discontinuities are naturally rare (e.g., a leading identity
+ * column in a composite index, a date column preceding a product_id column),
+ * then it makes sense for skip scans to optimistically assume that the next
+ * distinct indexable value will find directly matching index tuples.
+ *
+ * The B-Tree code can fall back on next-key sentinel values for any opclass
+ * that doesn't provide its own skip support function.  There is no point in
+ * providing skip support unless the next indexed key value is often the next
+ * indexable value (at least with some workloads).  Opclasses where that never
+ * works out in practice should just rely on the B-Tree AM's generic next-key
+ * fallback strategy.  Opclasses where adding skip support is infeasible or
+ * hard (e.g., an opclass for a continuous type) can also use the fallback.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called.
+ * If an opclass can only set some of the fields, then it cannot safely
+ * provide a skip support routine.
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming standard ascending
+	 * order).  This helps the B-Tree code with finding its initial position
+	 * at the leaf level (during the skip scan's first primitive index scan).
+	 * In other words, it gives the B-Tree code a useful value to start from,
+	 * before any data has been read from the index.
+	 *
+	 * low_elem and high_elem are also used by skip scans to determine when
+	 * they've reached the final possible value (in the current direction).
+	 * It's typical for the scan to run out of leaf pages before it runs out
+	 * of unscanned indexable values, but it's still useful for the scan to
+	 * have a way to recognize when it has reached the last possible value
+	 * (this saves us a useless probe that just lands on the final leaf page).
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (in the case of pass-by-reference
+	 * types).  It's not okay for these functions to leak any memory.
+	 *
+	 * Both decrement and increment callbacks are guaranteed to never be
+	 * called with a NULL "existing" arg.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is undefined, and the B-Tree
+	 * code is entitled to assume that no memory will have been allocated.
+	 *
+	 * The B-Tree skip scan caller's "existing" datum is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must be liberal
+	 * in accepting every possible representational variation within the
+	 * underlying data type.  On the other hand, opclasses are _not_ expected
+	 * to preserve any information that doesn't affect how datums are sorted
+	 * (e.g., skip support for a fixed precision numeric type isn't required
+	 * to preserve datum display scale).
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index dcd04b813..dc99dad29 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..26ebbf776 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 4ecf883d3..e4e2bb741 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -32,6 +32,7 @@
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
 #include "storage/smgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -71,7 +72,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* instrumentation */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -79,11 +80,21 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The reset of the space allocated in shared memory is also used when
+	 * scans need to schedule another primitive index scan.  It holds a
+	 * flattened representation of the backend's skip array datums, if any.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -538,10 +549,155 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
 	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Assume every index attribute might require that we generate a skip scan
+	 * key
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		Form_pg_attribute attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/* Restore skip array */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+
+		/* Now that old sk_argument memory is freed, copy over sk_flags */
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument to serialize */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -552,7 +708,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_scanPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	bt_target->btps_nsearches = 0;
@@ -574,15 +731,15 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_scanPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -609,6 +766,7 @@ btparallelrescan(IndexScanDesc scan)
 bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false;
 	bool		status = true;
@@ -642,7 +800,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -657,14 +815,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				*pageno = InvalidBlockNumber;
 				exit_loop = true;
 			}
@@ -701,7 +854,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			*pageno = btscan->btps_scanPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -731,10 +884,10 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber scan_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_scanPage = scan_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -771,7 +924,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -780,7 +933,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -798,6 +951,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -807,7 +961,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_scanPage == prev_scan_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -816,14 +970,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index e5f941e0a..ed5593c62 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -883,7 +883,6 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	Buffer		buf;
 	BTStack		stack;
 	OffsetNumber offnum;
-	StrategyNumber strat;
 	BTScanInsertData inskey;
 	ScanKey		startKeys[INDEX_MAX_KEYS];
 	ScanKeyData notnullkeys[INDEX_MAX_KEYS];
@@ -975,7 +974,20 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * a > or < boundary or find an attribute with no boundary (which can be
 	 * thought of as the same as "> -infinity"), we can't use keys for any
 	 * attributes to its right, because it would break our simplistic notion
-	 * of what initial positioning strategy to use.
+	 * of what initial positioning strategy to use.  In practice skip scan
+	 * typically enables us to use all scan keys here, even with a set of
+	 * input keys that leave a "gap" between two index attributes (cases with
+	 * multiple gaps will even manage this without any special restrictions).
+	 *
+	 * Skip scan works by having _bt_preprocess_keys cons up = boundary keys
+	 * for any index columns that were missing a = key in scan->keyData[], the
+	 * input scan keys passed to us by the executor.  This happens for index
+	 * attributes prior to the attribute of our final input scan key.  The
+	 * underlying = keys use skip arrays.  The keys can be thought of as the
+	 * same as "col = ANY('{every possible col value}')".  Note that this
+	 * often includes the array element NULL, which the scan will treat as an
+	 * IS NULL qual (the skip array's scan key is already marked SK_SEARCHNULL
+	 * when we're called, so we need no special handling for this case here).
 	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
@@ -1050,6 +1062,47 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		{
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					/* -inf/+inf element from a skip array's scan key */
+					ScanKey		origchosen = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == chosen - so->keyData)
+							break;
+					}
+
+					/* use array's inequality key in startKeys[] */
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					Assert(!chosen ||
+						   chosen->sk_attno == origchosen->sk_attno);
+
+					if (!array->null_elem)
+					{
+						/*
+						 * The array does not include a NULL element (meaning
+						 * array advancement never generates an IS NULL qual).
+						 * We'll deduce a NOT NULL key to skip over any NULLs
+						 * when there's no usable low_compare (or no usable
+						 * high_compare, during a backwards scan).
+						 *
+						 * Note: this also handles an explicit NOT NULL key
+						 * that preprocessing folded into the skip array (it
+						 * doesn't save them in low_compare/high_compare).
+						 */
+						impliesNN = origchosen;
+					}
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
 				/*
 				 * Done looking at keys for curattr.  If we didn't find a
 				 * usable boundary key, see if we can deduce a NOT NULL key.
@@ -1083,16 +1136,42 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 				startKeys[keysz++] = chosen;
 
+				if (chosen->sk_flags & SK_BT_NEXTPRIOR)
+				{
+					/*
+					 * Next/prior key element from a skip array's scan key.
+					 * 'chosen' could be SK_ISNULL, in which case startKeys[]
+					 * positions us at the first tuple > NULL (for backwards
+					 * scans it's the first tuple < NULL instead).
+					 *
+					 * Adjust strat_total, so that our = key gets treated like
+					 * a > key (or like a < key) within _bt_search.
+					 */
+					Assert(strat_total == BTEqualStrategyNumber);
+					if (ScanDirectionIsForward(dir))
+						strat_total = BTGreaterStrategyNumber;
+					else
+						strat_total = BTLessStrategyNumber;
+
+					/*
+					 * We'll never find an exact = match for a NEXTPRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[]
+					 * (besides, doing so would confuse _bt_search, since it
+					 * isn't directly aware of NEXTPRIOR sentinel values)
+					 */
+					break;
+				}
+
 				/*
 				 * Adjust strat_total, and quit if we have stored a > or <
 				 * key.
 				 */
-				strat = chosen->sk_strategy;
-				if (strat != BTEqualStrategyNumber)
+				if (chosen->sk_strategy != BTEqualStrategyNumber)
 				{
-					strat_total = strat;
-					if (strat == BTGreaterStrategyNumber ||
-						strat == BTLessStrategyNumber)
+					strat_total = chosen->sk_strategy;
+					if (chosen->sk_strategy == BTGreaterStrategyNumber ||
+						chosen->sk_strategy == BTLessStrategyNumber)
 						break;
 				}
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index d1423bd85..ab8f8f8c5 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND c > 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c' (and so 'c' will still have an inequality scan key,
+ * required in only one direction -- 'c' won't be output as a "range" skip
+ * key/array).
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using opclass skip
+ * support routines.  This can be used to quantify the peformance benefit that
+ * comes from having dedicated skip support, with a given test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup set to valid routine? */
+	Oid			eq_op;			/* InvalidOid means don't skip */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -64,15 +92,38 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno,
+							BTSkipPreproc *skipatts);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
-										   Datum arrdatum, ScanKey cur);
+										   Datum arrdatum, bool arrnull,
+										   ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -258,11 +309,19 @@ _bt_freestack(BTStack stack)
  * preprocessing steps are complete.  This will convert the scan key offset
  * references into references to the scan's so->keyData[] output scan keys.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by later preprocessing on output.
+ * _bt_decide_skipatts decides which attributes receive skip arrays.
+ *
  * Caller must pass *numberOfKeys to give us a way to change the number of
  * input scan keys (our output is caller's input).  The returned array can be
  * smaller than scan->keyData[] when we eliminated a redundant array scan key
- * (redundant with some other array scan key, for the same attribute).  Caller
- * uses this to allocate so->keyData[] for the current btrescan.
+ * (redundant with some other array scan key, for the same attribute).  It can
+ * also be larger when we added a skip array/skip scan key.  Caller uses this
+ * to allocate so->keyData[] for the current btrescan.
  *
  * Note: the reason we need to return a temp scan key array, rather than just
  * scribbling on scan->keyData, is that callers are permitted to call btrescan
@@ -275,8 +334,11 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 	Relation	rel = scan->indexRelation;
 	int			numArrayKeyData = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
+				numSkipArrayKeys,
 				output_ikey = 0;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -286,7 +348,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 
 	Assert(scan->numberOfKeys);
 
-	/* Quick check to see if there are any array keys */
+	/*
+	 * Quick check to see if there are any array keys, or any missing keys we
+	 * can generate a "skip scan" array key for ourselves
+	 */
 	numArrayKeys = 0;
 	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
@@ -304,6 +369,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 		}
 	}
 
+	numSkipArrayKeys = _bt_decide_skipatts(scan, skipatts);
+	if (numSkipArrayKeys)
+	{
+		/* At least one skip array scan key must be added to arrayKeyData[] */
+		numArrayKeys += numSkipArrayKeys;
+		/* output scan key buffer allocation needs space for skip scan keys */
+		numArrayKeyData += numSkipArrayKeys;
+	}
+
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
@@ -330,7 +404,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
 	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/*
+	 * Process each array key, and generate skip arrays as needed.  Also copy
+	 * every scan->keyData[] input scan key (whether it's an array or not)
+	 * into the arrayKeyData array we'll return to our caller (barring any
+	 * array scan keys that we could eliminate early through array merging).
+	 */
 	numArrayKeys = 0;
 	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
@@ -348,8 +427,76 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and scan key where indicated by skipatts */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* won't skip using this attribute */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[output_ikey];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * Temporary testing GUC can disable the use of skip support
+			 * routines
+			 */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how the
+			 * consed-up "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[output_ikey], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			output_ikey++;		/* keep this scan key/array */
+		}
+
 		/*
-		 * Copy input scan key into temp arrayKeyData scan key array
+		 * Copy input scan key into temp arrayKeyData scan key array.  (From
+		 * here on, cur points at our copy of the input scan key.)
 		 */
 		cur = &arrayKeyData[output_ikey];
 		*cur = scan->keyData[input_ikey];
@@ -521,6 +668,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *numberOfKeys)
 		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
 		numArrayKeys++;
 		output_ikey++;			/* keep this scan key/array */
 	}
@@ -634,7 +785,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -685,7 +837,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -695,6 +847,192 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_decide_skipatts() -- set index attributes requiring skip arrays
+ *
+ * _bt_preprocess_array_keys helper function.  Determines which attributes
+ * will require skip arrays/scan keys.  Also sets up skip support callbacks
+ * for attributes whose input opclass have skip support (opclasses without
+ * skip support will fall back on using next-key sentinel values when
+ * advancing the skip array to its next array element).
+ *
+ * Return value is the total number of scan keys to add as "input" scan keys
+ * for further processing within _bt_preprocess_keys.
+ */
+static int
+_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts)
+{
+	Relation	rel = scan->indexRelation;
+	ScanKey		inputsk;
+	AttrNumber	attno_inputsk = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	inputsk = &scan->keyData[0];
+	for (int i = 0;; inputsk++, i++)
+	{
+		int			prev_numSkipArrayKeys = numSkipArrayKeys;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inputsk
+		 */
+		while (attno_skip < attno_inputsk)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				return prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			numSkipArrayKeys++;
+			attno_skip++;
+		}
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i >= scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 * (either in part or in whole)
+		 */
+		if (attno_inputsk > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inputsk (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inputsk < inputsk->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				numSkipArrayKeys++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				return numSkipArrayKeys;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inputsk = inputsk->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inputsk->sk_strategy == BTEqualStrategyNumber ||
+			(inputsk->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required.)
+		 */
+		if (inputsk->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	return numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatts
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatts)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatts->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										  BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack an equality operator, but
+	 * they're supported.  Cope with them by having caller not use skip scan.
+	 */
+	if (!OidIsValid(skipatts->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatts->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+														reverse,
+														&skipatts->sksup);
+
+	/* might not have set up skip support routine, but can skip either way */
+	return true;
+}
+
 /*
  * _bt_setup_array_cmp() -- Set up array comparison functions
  *
@@ -987,17 +1325,15 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
@@ -1010,8 +1346,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.  We have to be sure
+	 * that _bt_compare_array_skey/_bt_binsrch_array_skey use the right proc.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1042,11 +1378,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensured that the scalar scan key could be eliminated as redundant
+		 */
+		eliminated = true;
+	}
+	else
+	{
+		/*
+		 * With a skip array it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when earlier preprocessing wasn't able to eliminate a
+		 * redundant scan key inequality due to a lack of cross-type support.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when it
+ * is redundant with (or contradicted by) a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1098,6 +1488,137 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of skip array scan key when it is "redundant with"
+ * a non-array scalar scan key.  The scalar scan key must be an inequality.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array, allowing its elements to be generated within the limits of a range.
+ * Calling here always renders caller's scalar scan key redundant (the key is
+ * applied when the array advances, but that's just an implementation detail).
+ *
+ * Return value indicates if the array already had a lower/upper bound
+ * (whichever caller's scalar scan key was expected to be).  We return true in
+ * the common case where caller's scan key could be successfully rolled into
+ * the skip array.  We return false when we can't do that due to the presence
+ * of a conflicting inequality.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	/*
+	 * We don't expect to have to deal with NULLs in non-array/non-skip scan
+	 * key.  We expect _bt_preprocess_array_keys to avoid generating a skip
+	 * array for an index attribute with an IS NULL input scan key.  (It will
+	 * still do so in the presence of IS NOT NULL input scan keys, but
+	 * _bt_compare_scankey_args is expected to handle those for us.)
+	 */
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(arraysk->sk_flags & SK_SEARCHARRAY);
+	Assert(arraysk->sk_strategy == BTEqualStrategyNumber);
+	Assert(array->num_elems == -1);
+
+	/* Scalar scan key must be a B-Tree inequality, which are always strict */
+	Assert(!(skey->sk_flags & SK_ISNULL));
+	Assert(skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * A skip array scan key always uses the underlying index attribute's
+	 * input opclass, but it's possible that caller's scalar scan key uses a
+	 * cross-type operator.  In cross-type scenarios, skey.sk_argument doesn't
+	 * use the same type as later array elements (which are all just copies of
+	 * datums taken from index tuples, possibly modified by skip support).
+	 *
+	 * We represent the lowest (and highest) possible value in the array using
+	 * the sentinel value -inf (+inf for high_compare).  The only exceptions
+	 * apply when the opclass has skip support: there we can use a copy of the
+	 * skip support routine's low_elem/high_elem instead -- though only when
+	 * there is no corresponding low_compare/high_compare inequality.
+	 *
+	 * _bt_first understands that -inf/+inf indicate that it should use the
+	 * low_compare/high_compare inequality for initial positioning purposes
+	 * when it sees either value (unless there is no corresponding inequality,
+	 * in which case the values are literally interpreted as -inf or +inf).
+	 * _bt_first can therefore vary in whether it uses a cross-type operator,
+	 * or an input-opclass-only operator (it can vary across primitive scans
+	 * for the same index attribute/skip array).
+	 *
+	 * _bt_scankey_decrement/_bt_scankey_increment both make sure that each
+	 * newly generated element is constrained by low_compare/high_compare.
+	 * This must happen without skey.sk_argument ever being treated as a true
+	 * array element (that wouldn't always work because array elements are
+	 * only ever supposed to use the opclass input type).
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare */
+
+				/* replace old high_compare with new one */
+			}
+			else
+				array->high_compare = palloc(sizeof(ScanKeyData));
+
+			memcpy(array->high_compare, skey, sizeof(ScanKeyData));
+			array->order_high = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare */
+
+				/* replace old low_compare with new one */
+			}
+			else
+				array->low_compare = palloc(sizeof(ScanKeyData));
+
+			memcpy(array->low_compare, skey, sizeof(ScanKeyData));
+			array->order_low = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
@@ -1140,7 +1661,8 @@ _bt_compare_array_elements(const void *a, const void *b, void *arg)
 static inline int32
 _bt_compare_array_skey(FmgrInfo *orderproc,
 					   Datum tupdatum, bool tupnull,
-					   Datum arrdatum, ScanKey cur)
+					   Datum arrdatum, bool arrnull,
+					   ScanKey cur)
 {
 	int32		result = 0;
 
@@ -1148,14 +1670,14 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 
 	if (tupnull)				/* NULL tupdatum */
 	{
-		if (cur->sk_flags & SK_ISNULL)
+		if (arrnull)
 			result = 0;			/* NULL "=" NULL */
 		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = -1;		/* NULL "<" NOT_NULL */
 		else
 			result = 1;			/* NULL ">" NOT_NULL */
 	}
-	else if (cur->sk_flags & SK_ISNULL) /* NOT_NULL tupdatum, NULL arrdatum */
+	else if (arrnull)			/* NOT_NULL tupdatum, NULL arrdatum */
 	{
 		if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = 1;			/* NOT_NULL ">" NULL */
@@ -1221,6 +1743,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1256,7 +1780,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[low_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result <= 0)
 				{
@@ -1284,7 +1808,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[high_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result >= 0)
 				{
@@ -1311,7 +1835,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 		arrdatum = array->elem_values[mid_elem];
 
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										arrdatum, cur);
+										arrdatum, false, cur);
 
 		if (result == 0)
 		{
@@ -1336,13 +1860,102 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	 */
 	if (low_elem != mid_elem)
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										array->elem_values[low_elem], cur);
+										array->elem_values[low_elem], false,
+										cur);
 
 	*set_elem_result = result;
 
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * This routine doesn't return an index into the array, because the array
+ * doesn't actually have any elements (it generates its array elements
+ * procedurally instead).  Note that this may include a NULL value/an IS NULL
+ * qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1352,29 +1965,486 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
 		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
 		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, curArrayKey,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppoDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Clear possibly-irrelevant flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Lowest (or highest) element is NULL, so set scan key to NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Lowest array element isn't NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Highest array element isn't NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		underflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & SK_BT_NEXTPRIOR));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the NEXTPRIOR flag.  The true prior value can only
+	 * be determined when the scan reads lower sorting tuples.
+	 *
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, _bt_first can find the highest non-NULL.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * low_compare is for an >= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true prior value
+		 * cannot possibly satisfy low_compare.  We can give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(&array->order_low,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, false,
+								   skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true prior value */
+		skey->sk_flags |= SK_BT_NEXTPRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &underflow);
+
+	if (underflow)
+	{
+		/* dec_sk_argument has undefined value due to underflow */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the skip array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		overflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & SK_BT_NEXTPRIOR));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXTPRIOR flag.  The true next value can only be
+	 * determined when the scan reads higher sorting tuples.
+	 *
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, _bt_first can find the lowest non-NULL.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * high_compare is for an <= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true next value cannot
+		 * possibly satisfy high_compare.  We can give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(&array->order_high,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, false,
+								   skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true next value */
+		skey->sk_flags |= SK_BT_NEXTPRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &overflow);
+
+	if (overflow)
+	{
+		/* inc_sk_argument has undefined value due to overflow */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the skip array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1390,6 +2460,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1399,29 +2470,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1476,6 +2548,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1483,7 +2556,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1495,16 +2567,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1568,6 +2634,8 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 	for (int ikey = sktrig; ikey < so->numberOfKeys; ikey++)
 	{
 		ScanKey		cur = so->keyData + ikey;
+		Datum		sk_argument = cur->sk_argument;
+		bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
 		Datum		tupdatum;
 		bool		tupnull;
 		int32		result;
@@ -1629,9 +2697,66 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_NEGPOSINF))
+		{
+			/* The scankey has a conventional sk_argument/element value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											sk_argument, sk_isnull, cur);
+
+			/*
+			 * When scan key is marked NEXTPRIOR, the current array element is
+			 * "sk_argument + infinitesimal" (or the current array element is
+			 * "sk_argument - infinitesimal", during backwards scans)
+			 */
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXTPRIOR))
+			{
+				/*
+				 * tupdatum is actually still < "sk_argument + infinitesimal"
+				 * (or it's actually still > "sk_argument - infinitesimal")
+				 */
+				return true;
+			}
+		}
+		else
+		{
+			/*
+			 * The scankey searches for the sentinel value -inf/+inf.
+			 *
+			 * Note: -inf could mean "absolute" -inf, or it could represent
+			 * the lowest possible value that still satisfies the array's
+			 * low_compare.  +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * Compare tupdatum against -inf using array's low_compare, if any
+			 * (or compare it against +inf using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is > -inf sk_argument (or < +inf sk_argument).
+				 * It's time for caller to advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1963,18 +3088,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1999,18 +3115,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2028,15 +3135,27 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
+			Datum		sk_argument = cur->sk_argument;
+			bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
+
 			Assert(sktrig_required && required);
 
 			/*
@@ -2050,7 +3169,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 */
 			result = _bt_compare_array_skey(&so->orderProcs[ikey],
 											tupdatum, tupnull,
-											cur->sk_argument, cur);
+											sk_argument, sk_isnull, cur);
 		}
 
 		/*
@@ -2109,11 +3228,65 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  This is often the range -inf through to +inf.
+		 * "Binary searching" a skip array only determines whether tupdatum is
+		 * beyond its range, before its range, or within its range.
+		 *
+		 * Note: conventional arrays cannot use this approach.  They need
+		 * "beyond end of array element" advancement to distinguish between
+		 * the final array element (where incremental advancement rolls over
+		 * to the next most significant array), and some earlier array element
+		 * (where incremental advancement just increments set_elem/cur_elem).
+		 * That distinction doesn't exist when dealing with range skip arrays.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == some particular skip array element.
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2464,6 +3637,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also cons up skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2587,8 +3762,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		inputsk = scan->keyData;
 
 	/*
-	 * Now that we have an estimate of the number of output scan keys,
-	 * allocate space for them
+	 * Now that we have an estimate of the number of output scan keys
+	 * (including any skip array scan keys), allocate space for them
 	 */
 	so->keyData = palloc(sizeof(ScanKeyData) * numberOfKeys);
 
@@ -2724,7 +3899,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
+						Assert(!array || array->num_elems > 0 ||
+							   array->num_elems == -1);
 						xform[j].skey = NULL;
 						xform[j].ikey = -1;
 					}
@@ -2887,7 +4063,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
+					Assert(!array || array->num_elems > 0 ||
+						   array->num_elems == -1);
 
 					/*
 					 * New key is more restrictive, and so replaces old key...
@@ -3029,10 +4206,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3107,6 +4285,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3180,6 +4374,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3743,6 +4938,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index db6ed784a..86a5de8f4 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 8efb4044d..6efa3e353 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -372,6 +372,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index edb09d4e3..e945686c8 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -96,6 +96,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 8130f3e8a..256350007 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index 8c6fc80c3..91682edd5 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -83,6 +83,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 03d7fb5f4..78864b15d 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -192,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -213,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5733,6 +5737,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6791,6 +6881,54 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a
+ * single-column index, or C * 0.75 for multiple columns. (The idea here
+ * is that multiple columns dilute the importance of the first column's
+ * ordering, but don't negate it entirely.  Before 8.0 we divided the
+ * correlation by the number of columns, but that seems too strong.)
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6800,17 +6938,21 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6826,13 +6968,17 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
@@ -6843,13 +6989,81 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6891,7 +7105,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6907,6 +7121,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6922,6 +7168,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -7030,104 +7277,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 5284d23dc..654bda638 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -384,6 +387,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 686309db5..faa3a678f 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1751,6 +1752,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3590,6 +3602,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..9662fb2ba 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -583,6 +583,19 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure via an index <quote>skip scan</quote>.  The
+     APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..433e108b8 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intevening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,10 +511,7 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
+   Multicolumn indexes should be used judiciously.  See
    <xref linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
@@ -669,9 +669,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..8b6b775c1 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index cf6eac573..f7b3ecef4 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1938,7 +1938,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -1958,7 +1958,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 31fb7d142..8c2a939b0 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4370,24 +4370,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -7482,19 +7483,23 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                        QUERY PLAN                         
------------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Nested Loop
    ->  Hash Join
          Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-         ->  Seq Scan on fkest f2
-               Filter: (x100 = 2)
+         ->  Bitmap Heap Scan on fkest f2
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
          ->  Hash
-               ->  Seq Scan on fkest f1
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f1
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
          Index Cond: (x = f1.x)
-(10 rows)
+(14 rows)
 
 alter table fkest add constraint fk
   foreign key (x, x10b, x100) references fkest (x, x10, x100);
@@ -7503,20 +7508,24 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                     QUERY PLAN                      
------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Hash Join
    Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
    ->  Hash Join
          Hash Cond: (f3.x = f2.x)
          ->  Seq Scan on fkest f3
          ->  Hash
-               ->  Seq Scan on fkest f2
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f2
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Hash
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-(11 rows)
+         ->  Bitmap Heap Scan on fkest f1
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
+(15 rows)
 
 rollback;
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 6aeb7cb96..f4c696ca5 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5193,9 +5193,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 0456d48c9..39aa1f89e 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1461,18 +1461,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..4246afefd 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index e296891ca..1d269dc30 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -766,7 +766,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -776,7 +776,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ace5414fa..bdaf4d62a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2662,6 +2663,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

In reply to: Tomas Vondra (#24)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Mon, Sep 16, 2024 at 6:05 PM Tomas Vondra <tomas@vondra.me> wrote:

I've been looking at this patch over the couple last days, mostly doing
some stress testing / benchmarking (hence the earlier report) and basic
review.

Thanks for taking a look! Very helpful.

I do have some initial review comments, and the testing produced
some interesting regressions (not sure if those are the cases where
skipscan can't really help, that Peter mentioned he needs to look into).

The one type of query that's clearly regressed in a way that's just
not acceptable are queries where we waste CPU cycles during scans
where it's truly hopeless. For example, I see a big regression on one
of the best cases for the Postgres 17 work, described here:

https://pganalyze.com/blog/5mins-postgres-17-faster-btree-index-scans#a-practical-example-3x-performance-improvement

Notably, these cases access exactly the same buffers/pages as before,
so this really isn't a matter of "doing too much skipping". The number
of buffers hit exactly matches what you'll see on Postgres 17. It's
just that we waste too many CPU cycles in code such as
_bt_advance_array_keys, to uselessly maintain skip arrays.

I'm not suggesting that there won't be any gray area with these
regressions -- nothing like this will ever be that simple. But it
seems to me like I should go fix these obviously-not-okay cases next,
and then see where that leaves everything else, regressions-wise. That
seems likely to be the most efficient way of dealing with the
regressions. So I'll start there.

That said, I *would* be surprised if you found a regression in any
query that simply didn't receive any new scan key transformations in
new preprocessing code in places like _bt_decide_skipatts and
_bt_skip_preproc_shrink. I see that many of the queries that you're
using for your stress-tests "aren't really testing skip scan", in this
sense. But I'm hardly about to tell you that you shouldn't spend time
on such queries -- that approach just discovered a bug affecting
Postgres 17 (that was also surprising, but it still happened!). My
point is that it's worth being aware of which test queries actually
use skip arrays in the first place -- it might help you with your
testing. There are essentially no changes to _bt_advance_array_keys
that'll affect traditional SAOP arrays (with the sole exception of
changes made by
v6-0003-Refactor-handling-of-nbtree-array-redundancies.patch, which
affect every kind of array in the same way).

1) v6-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patch

- I find the places that increment "nsearches" a bit random. Each AM
does it in entirely different place (at least it seems like that to me).
Is there a way make this a bit more consistent?

From a mechanical perspective there is nothing at all random about it:
we do this at precisely the same point that we currently call
pgstat_count_index_scan, which in each index AM maps to one descent of
the index. It is at least consistent. Whenever a B-Tree index scan
shows "Index Scans: N", you'll see precisely the same number by
swapping it with an equivalent contrib/btree_gist-based GiST index and
running the same query again (assuming that index tuples that match
the array keys are spread apart in both the B-Tree and GiST indexes).

(Though I see problems with the precise place that nbtree calls
pgstat_count_index_scan right now, at least in certain edge-cases,
which I discuss below in response to your questions about that.)

uint64 btps_nsearches; /* instrumentation */

Instrumentation what? What's the counter for?

Will fix.

In case you missed it, there is another thread + CF Entry dedicated to
discussing this instrumentation patch:

https://commitfest.postgresql.org/49/5183/
/messages/by-id/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com

- I see _bt_first moved the pgstat_count_index_scan, but doesn't that
mean we skip it if the earlier code does "goto readcomplete"? Shouldn't
that still count as an index scan?

In my opinion, no, it should not.

We're counting the number of times we'll have descended the tree using
_bt_search (or using _bt_endpoint, perhaps), which is a precisely
defined physical cost. A little like counting the number of buffers
accessed. I actually think that this aspect of how we call
pgstat_count_index_scan is a bug that should be fixed, with the fix
backpatched to Postgres 17. Right now, we see completely different
counts for a parallel index scan, compared to an equivalent serial
index scan -- differences that cannot be explained as minor
differences caused by parallel scan implementation details. I think
that it's just wrong right now, on master, since we're simply not
counting the thing that we're supposed to be counting (not reliably,
not if it's a parallel index scan).

- show_indexscan_nsearches does this:

if (scanDesc && scanDesc->nsearches > 0)
ExplainPropertyUInteger("Index Searches", NULL,
scanDesc->nsearches, es);

But shouldn't it divide the count by nloops, similar to (for example)
show_instrumentation_count?

I can see arguments for and against doing it that way. It's
ambiguous/subjective, but on balance I favor not dividing by nloops.
You can make a similar argument for doing this with "Buffers: ", and
yet we don't divide by nloops there, either.

Honestly, I just want to find a way to do this that everybody can live
with. Better documentation could help here.

2) v6-0002-Normalize-nbtree-truncated-high-key-array-behavio.patch

- Admittedly very subjective, but I find the "oppoDirCheck" abbreviation
rather weird, I'd just call it "oppositeDirCheck".

Will fix.

3) v6-0003-Refactor-handling-of-nbtree-array-redundancies.patch

- nothing

Great. I think that I should be able to commit this one soon, since
it's independently useful work.

4) v6-0004-Add-skip-scan-to-nbtree.patch

- indices.sgml seems to hahve typo "Intevening" -> "Intervening"

- It doesn't seem like a good idea to remove the paragraph about
multicolumn indexes and replace it with just:

Multicolumn indexes should be used judiciously.

I mean, what does judiciously even mean? what should the user consider
to be judicious? Seems rather unclear to me. Admittedly, the old text
was not much helpful, but at least it gave some advice.

Yeah, this definitely needs more work.

But maybe more importantly, doesn't skipscan apply only to a rather
limited subset of data types (that support increment/decrement)? Doesn't
the new wording mostly ignore that, implying skipscan applies to all
btree indexes? I don't think it mentions datatypes anywhere, but there
are many indexes on data types like text, UUID and so on.

Actually, no, skip scan works in almost the same way with all data
types. Earlier versions of the patch didn't support every data type
(perhaps I should have waited for that before posting my v1), but the
version of the patch you looked at has no restrictions on any data
type.

You must be thinking of whether or not an opclass has skip support.
That's just an extra optimization, which can be used for a small
handful of discrete data types such as integer and date (hard to
imagine how skip support could ever be implemented for types like
numeric and text). There is a temporary testing GUC that will allow
you to get a sense of how much skip support can help: try "set
skipscan_skipsupport_enabled=off" with (say) my original MDAM test
query to get a sense of that. You'll see more buffer hits needed for
"next key probes", though not dramatically more.

It's worth having skip support (the idea comes from the MDAM paper),
but it's not essential. Whether or not an opclass has skip support
isn't accounted for by the cost model, but I doubt that it's worth
addressing (the cost model is already pessimistic).

- Very subjective nitpicking, but I find it a bit strange when a comment
about a block is nested in the block, like in _bt_first() for the
array->null_elem check.

Will fix.

- assignProcTypes() claims providing skipscan for cross-type scenarios
doesn't make sense. Why is that? I'm not saying the claim is wrong, but
it's not clear to me why would that be the case.

It is just talking about the support function that skip scan can
optionally use, where it makes sense (skip support functions). The
relevant "else if (member->number == BTSKIPSUPPORT_PROC)" stanza is
largely copied from the existing nearby "else if (member->number ==
BTEQUALIMAGE_PROC)" stanza that was added for B-Tree deduplication. In
both stanzas we're talking about a capability that maps to a
particular "input opclass", which means the opclass that maps to the
datums that are stored on disk, in index tuples.

There are no restrictions on the use of skip scan with queries that
happen to involve the use of cross-type operators. It doesn't even
matter if we happen to be using an incomplete opfamily, since range
skip arrays never need to *directly* take the current array element
from a lower/upper bound inequality scan key's argument. It all
happens indirectly: code in places like _bt_first and _bt_checkkeys
can use inequalities (which are stored in BTArrayKeyInfo.low_compare
and BTArrayKeyInfo.high_compare) to locate the next matching on-disk
index tuple that satisfies the inequality in question. Obviously, the
located datum must be the same type as the one used by the array and
its scan key (it has to be the input opclass type if it's taken from
an index tuple).

I think that it's a bit silly that nbtree generally bends over
backwards to find a way to execute a scan, given an incomplete
opfamily; in a green field situation it would make sense to just throw
an error instead. Even still, skip scan works in a way that is
maximally forgiving when incomplete opfamilies are used. Admittedly,
it is just about possible to come up with a scenario where we'll now
throw an error for a query that would have worked on Postgres 17. But
that's no different to what would happen if the query had an explicit
"= any( )" non-cross-type array instead of an implicit non-cross-type
skip array. The real problem in these scenarios is the lack of a
suitable cross-type ORDER proc (for a cross-type-operator query)
within _bt_first -- not the lack of cross-type operators. This issue
with missing ORDER procs just doesn't seem worth worrying about,
since, as I said, even slightly different queries (that don't use skip
scan) are bound to throw the same errors either way.

Peter asked me to look at the costing, and I think it looks generally
sensible.

I'm glad that you think that I basically have the right idea here.
Hard to know how to approach something like this, which doesn't have
any kind of precedent to draw on.

We don't really have a lot of information to base the costing
on in the first place - the whole point of skipscan is about multicolumn
indexes, but none of the existing extended statistic seems very useful.
We'd need some cross-column correlation info, or something like that.

Maybe, but that would just mean that we'd sometimes be more optimistic
about skip scan helping than we are with the current approach of
pessimistically assuming that there is no correlation at all. Not
clear that being pessimistic in this sense isn't the right thing to
do, despite the fact that it's clearly less accurate on average.

There's one thing that I don't quite understand, and that's how
btcost_correlation() adjusts correlation for multicolumn indexes:

if (index->nkeycolumns > 1)
indexCorrelation = varCorrelation * 0.75;

That seems fine for a two-column index, I guess. But shouldn't it
compound for indexes with more keys? I mean, 0.75 * 0.75 for third
column, etc? I don't think btcostestimate() does that, it just remembers
whatever btcost_correlation() returns.

I don't know either. In general I'm out of my comfort zone here.

The only alternative approach I can think of is not to adjust the
costing for the index scan at all, and only use this to enable (or not
enable) the skipscan internally. That would mean the overall plan
remains the same, and maybe sometimes we would think an index scan would
be too expensive and use something else. Not great, but it doesn't have
the risk of regressions - IIUC we can disable the skipscan at runtime,
if we realize it's not really helpful.

In general I would greatly prefer to not have a distinct kind of index
path for scans that use skip scan. I'm quite keen on a design that
allows the scan to adapt to unpredictable conditions at runtime.

Of course, that doesn't preclude passing the index scan a hint about
what's likely to work at runtime, based on information figured out
when costing the scan. Perhaps that will prove necessary to avoid
regressing index scans that are naturally quite cheap already -- scans
where we really need to have the right general idea from the start to
avoid any regressions. I'm not opposed to that, provided the index
scan has the ability to change its mind when (for whatever reason) the
guidance from the optimizer turns out to be wrong.

As usual, I wrote a bash script to do a bit of stress testing. It
generates tables with random data, and then runs random queries with
random predicates on them, while mutating a couple parameters (like
number of workers) to trigger different plans. It does that on 16,
master and with the skipscan patch (with the fix for parallel scans).

I wonder if some of the regressions you see can be tied to the use of
an LWLock in place of the existing use of a spin lock. I did that
because I sometimes need to allocate memory to deserialize the array
keys, with the exclusive lock held. It might be the case that a lot of
these regressions are tied to that, or something else that is far from
obvious...have to investigate.

In general, I haven't done much on parallel index scans here (I only
added support for them very recently), whereas your testing places a
lot of emphasis on parallel scans. Nothing wrong with that emphasis
(it caught that 17 bug), but just want to put it in context.

I've uploaded the script and results from the last run here:

https://github.com/tvondra/pg-skip-scan-tests

There's the "run-mdam.sh" script that generates tables/queries, runs
them, collects all kinds of info about the query, and produces files
with explain plans, CSV with timings, etc.

It'll take me a while to investigate all this data.

Anyway, I ran a couple thousand such queries, and I haven't found any
incorrect results (the script compares that between versions too). So
that's good ;-)

That's good!

But my main goal was to see how this affects performance. The tables
were pretty small (just 1M rows, maybe ~80MB), but with restarts and
dropping caches, large enough to test this.

The really compelling cases all tend to involve fairly selective index
scans. Obviously, skip scan can only save work by navigating the index
structure more efficiently (unlike loose index scan). So if the main
cost is inherently bound to be the cost of heap accesses, then we
shouldn't expect a big speed up.

For example, one of the slowed down queries is query 702 (top of page 8
in the PDF). The query is pretty simple:

explain (analyze, timing off, buffers off)
select id1,id2 from t_1000000_1000_1_2
where NOT (id1 in (:list)) AND (id2 = :value);

and it was executed on a table with random data in two columns, each
with 1000 distinct values. This is perfectly random data, so a great
match for the assumptions in costing etc.

But with uncached data, this runs in ~50 ms on master, but takes almost
200 ms with skipscan (these timings are from my laptop, but similar to
the results).

I'll need to investigate this specifically. That does seem odd.

FWIW, it's a pity that the patch doesn't know how to push down the NOT
IN () here. The MDAM paper contemplates such a scheme. We see the use
of filter quals here, when in principle this could work by using a
skip array that doesn't generate elements that appear in the NOT IN()
list (it'd generate every possible indexable value *except* the given
list/array values). The only reason that I haven't implemented this
yet is because I'm not at all sure how to make it work on the
optimizer side. The nbtree side of the implementation will probably be
quite straightforward, since it's really just a slight variant of a
skip array, that excludes certain values.

-- with skipscan
Index Only Scan using t_1000000_1000_1_2_id1_id2_idx on
t_1000000_1000_1_2 (cost=0.96..983.26 rows=1719 width=16)
(actual rows=811 loops=1)
Index Cond: (id2 = 997)
Index Searches: 1007
Filter: (id1 <> ALL ('{983,...,640}'::bigint[]))
Rows Removed by Filter: 163
Heap Fetches: 0
Planning Time: 3.730 ms
Execution Time: 238.554 ms
(8 rows)

I haven't looked into why this is happening, but this seems like a
pretty good match for skipscan (on the first column). And for the
costing too - it's perfectly random data, no correllation, etc.

I wonder what "Buffers: N" shows? That's usually the first thing I
look at (that and "Index Searches", which looks like what you said it
should look like here). But, yeah, let me get back to you on this.

Thanks again!
--
Peter Geoghegan

#28Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#27)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 9/18/24 00:14, Peter Geoghegan wrote:

On Mon, Sep 16, 2024 at 6:05 PM Tomas Vondra <tomas@vondra.me> wrote:

I've been looking at this patch over the couple last days, mostly doing
some stress testing / benchmarking (hence the earlier report) and basic
review.

Thanks for taking a look! Very helpful.

I do have some initial review comments, and the testing produced
some interesting regressions (not sure if those are the cases where
skipscan can't really help, that Peter mentioned he needs to look into).

The one type of query that's clearly regressed in a way that's just
not acceptable are queries where we waste CPU cycles during scans
where it's truly hopeless. For example, I see a big regression on one
of the best cases for the Postgres 17 work, described here:

https://pganalyze.com/blog/5mins-postgres-17-faster-btree-index-scans#a-practical-example-3x-performance-improvement

Notably, these cases access exactly the same buffers/pages as before,
so this really isn't a matter of "doing too much skipping". The number
of buffers hit exactly matches what you'll see on Postgres 17. It's
just that we waste too many CPU cycles in code such as
_bt_advance_array_keys, to uselessly maintain skip arrays.

I'm not suggesting that there won't be any gray area with these
regressions -- nothing like this will ever be that simple. But it
seems to me like I should go fix these obviously-not-okay cases next,
and then see where that leaves everything else, regressions-wise. That
seems likely to be the most efficient way of dealing with the
regressions. So I'll start there.

That said, I *would* be surprised if you found a regression in any
query that simply didn't receive any new scan key transformations in
new preprocessing code in places like _bt_decide_skipatts and
_bt_skip_preproc_shrink. I see that many of the queries that you're
using for your stress-tests "aren't really testing skip scan", in this
sense. But I'm hardly about to tell you that you shouldn't spend time
on such queries -- that approach just discovered a bug affecting
Postgres 17 (that was also surprising, but it still happened!). My
point is that it's worth being aware of which test queries actually
use skip arrays in the first place -- it might help you with your
testing. There are essentially no changes to _bt_advance_array_keys
that'll affect traditional SAOP arrays (with the sole exception of
changes made by
v6-0003-Refactor-handling-of-nbtree-array-redundancies.patch, which
affect every kind of array in the same way).

Makes sense. I started with the testing before before even looking at
the code, so it's mostly a "black box" approach. I did read the 1995
paper before that, and the script generates queries with clauses
inspired by that paper, in particular:

- col = $value
- col IN ($values)
- col BETWEEN $value AND $value
- NOT (clause)
- clause [AND|OR] clause

There certainly may be gaps and interesting cases the script does not
cover. Something to improve.

1) v6-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patch

- I find the places that increment "nsearches" a bit random. Each AM
does it in entirely different place (at least it seems like that to me).
Is there a way make this a bit more consistent?

From a mechanical perspective there is nothing at all random about it:
we do this at precisely the same point that we currently call
pgstat_count_index_scan, which in each index AM maps to one descent of
the index. It is at least consistent. Whenever a B-Tree index scan
shows "Index Scans: N", you'll see precisely the same number by
swapping it with an equivalent contrib/btree_gist-based GiST index and
running the same query again (assuming that index tuples that match
the array keys are spread apart in both the B-Tree and GiST indexes).

(Though I see problems with the precise place that nbtree calls
pgstat_count_index_scan right now, at least in certain edge-cases,
which I discuss below in response to your questions about that.)

OK, understood. FWIW I'm not saying these places are "wrong", just that
it feels each AM does that in a very different place.

uint64 btps_nsearches; /* instrumentation */

Instrumentation what? What's the counter for?

Will fix.

In case you missed it, there is another thread + CF Entry dedicated to
discussing this instrumentation patch:

https://commitfest.postgresql.org/49/5183/
/messages/by-id/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com

Thanks, I wasn't aware of that.

- I see _bt_first moved the pgstat_count_index_scan, but doesn't that
mean we skip it if the earlier code does "goto readcomplete"? Shouldn't
that still count as an index scan?

In my opinion, no, it should not.

We're counting the number of times we'll have descended the tree using
_bt_search (or using _bt_endpoint, perhaps), which is a precisely
defined physical cost. A little like counting the number of buffers
accessed. I actually think that this aspect of how we call
pgstat_count_index_scan is a bug that should be fixed, with the fix
backpatched to Postgres 17. Right now, we see completely different
counts for a parallel index scan, compared to an equivalent serial
index scan -- differences that cannot be explained as minor
differences caused by parallel scan implementation details. I think
that it's just wrong right now, on master, since we're simply not
counting the thing that we're supposed to be counting (not reliably,
not if it's a parallel index scan).

OK, understood. If it's essentially an independent issue (perhaps even
counts as a bug?) what about correcting it on master first? Doesn't
sound like something we'd backpatch, I guess.

- show_indexscan_nsearches does this:

if (scanDesc && scanDesc->nsearches > 0)
ExplainPropertyUInteger("Index Searches", NULL,
scanDesc->nsearches, es);

But shouldn't it divide the count by nloops, similar to (for example)
show_instrumentation_count?

I can see arguments for and against doing it that way. It's
ambiguous/subjective, but on balance I favor not dividing by nloops.
You can make a similar argument for doing this with "Buffers: ", and
yet we don't divide by nloops there, either.

Honestly, I just want to find a way to do this that everybody can live
with. Better documentation could help here.

Seems like a bit of a mess. IMHO we should either divide everything by
nloops (so that everything is "per loop", or not divide anything. My
vote would be to divide, but that's mostly my "learned assumption" from
the other fields. But having a 50:50 split is confusing for everyone.

2) v6-0002-Normalize-nbtree-truncated-high-key-array-behavio.patch

- Admittedly very subjective, but I find the "oppoDirCheck" abbreviation
rather weird, I'd just call it "oppositeDirCheck".

Will fix.

3) v6-0003-Refactor-handling-of-nbtree-array-redundancies.patch

- nothing

Great. I think that I should be able to commit this one soon, since
it's independently useful work.

+1

4) v6-0004-Add-skip-scan-to-nbtree.patch

- indices.sgml seems to hahve typo "Intevening" -> "Intervening"

- It doesn't seem like a good idea to remove the paragraph about
multicolumn indexes and replace it with just:

Multicolumn indexes should be used judiciously.

I mean, what does judiciously even mean? what should the user consider
to be judicious? Seems rather unclear to me. Admittedly, the old text
was not much helpful, but at least it gave some advice.

Yeah, this definitely needs more work.

But maybe more importantly, doesn't skipscan apply only to a rather
limited subset of data types (that support increment/decrement)? Doesn't
the new wording mostly ignore that, implying skipscan applies to all
btree indexes? I don't think it mentions datatypes anywhere, but there
are many indexes on data types like text, UUID and so on.

Actually, no, skip scan works in almost the same way with all data
types. Earlier versions of the patch didn't support every data type
(perhaps I should have waited for that before posting my v1), but the
version of the patch you looked at has no restrictions on any data
type.

You must be thinking of whether or not an opclass has skip support.
That's just an extra optimization, which can be used for a small
handful of discrete data types such as integer and date (hard to
imagine how skip support could ever be implemented for types like
numeric and text). There is a temporary testing GUC that will allow
you to get a sense of how much skip support can help: try "set
skipscan_skipsupport_enabled=off" with (say) my original MDAM test
query to get a sense of that. You'll see more buffer hits needed for
"next key probes", though not dramatically more.

It's worth having skip support (the idea comes from the MDAM paper),
but it's not essential. Whether or not an opclass has skip support
isn't accounted for by the cost model, but I doubt that it's worth
addressing (the cost model is already pessimistic).

I admit I'm a bit confused. I probably need to reread the paper, but my
impression was that the increment/decrement is required for skipscan to
work. If we can't do that, how would it generate the intermediate values
to search for? I imagine it would be possible to "step through" the
index, but I thought the point of skip scan is to not do that.

Anyway, probably a good idea for extending the stress testing script.
Right now it tests with "bigint" columns only.

- Very subjective nitpicking, but I find it a bit strange when a comment
about a block is nested in the block, like in _bt_first() for the
array->null_elem check.

Will fix.

- assignProcTypes() claims providing skipscan for cross-type scenarios
doesn't make sense. Why is that? I'm not saying the claim is wrong, but
it's not clear to me why would that be the case.

It is just talking about the support function that skip scan can
optionally use, where it makes sense (skip support functions). The
relevant "else if (member->number == BTSKIPSUPPORT_PROC)" stanza is
largely copied from the existing nearby "else if (member->number ==
BTEQUALIMAGE_PROC)" stanza that was added for B-Tree deduplication. In
both stanzas we're talking about a capability that maps to a
particular "input opclass", which means the opclass that maps to the
datums that are stored on disk, in index tuples.

There are no restrictions on the use of skip scan with queries that
happen to involve the use of cross-type operators. It doesn't even
matter if we happen to be using an incomplete opfamily, since range
skip arrays never need to *directly* take the current array element
from a lower/upper bound inequality scan key's argument. It all
happens indirectly: code in places like _bt_first and _bt_checkkeys
can use inequalities (which are stored in BTArrayKeyInfo.low_compare
and BTArrayKeyInfo.high_compare) to locate the next matching on-disk
index tuple that satisfies the inequality in question. Obviously, the
located datum must be the same type as the one used by the array and
its scan key (it has to be the input opclass type if it's taken from
an index tuple).

I think that it's a bit silly that nbtree generally bends over
backwards to find a way to execute a scan, given an incomplete
opfamily; in a green field situation it would make sense to just throw
an error instead. Even still, skip scan works in a way that is
maximally forgiving when incomplete opfamilies are used. Admittedly,
it is just about possible to come up with a scenario where we'll now
throw an error for a query that would have worked on Postgres 17. But
that's no different to what would happen if the query had an explicit
"= any( )" non-cross-type array instead of an implicit non-cross-type
skip array. The real problem in these scenarios is the lack of a
suitable cross-type ORDER proc (for a cross-type-operator query)
within _bt_first -- not the lack of cross-type operators. This issue
with missing ORDER procs just doesn't seem worth worrying about,
since, as I said, even slightly different queries (that don't use skip
scan) are bound to throw the same errors either way.

OK. Thanks for the explanation. I'll think about maybe testing such
queries too (with cross-type clauses).

Peter asked me to look at the costing, and I think it looks generally
sensible.

I'm glad that you think that I basically have the right idea here.
Hard to know how to approach something like this, which doesn't have
any kind of precedent to draw on.

We don't really have a lot of information to base the costing
on in the first place - the whole point of skipscan is about multicolumn
indexes, but none of the existing extended statistic seems very useful.
We'd need some cross-column correlation info, or something like that.

Maybe, but that would just mean that we'd sometimes be more optimistic
about skip scan helping than we are with the current approach of
pessimistically assuming that there is no correlation at all. Not
clear that being pessimistic in this sense isn't the right thing to
do, despite the fact that it's clearly less accurate on average.

Hmmm, yeah. I think it'd be useful to explain this reasoning (assuming
no correlation means pessimistic skipscan costing) in a comment before
btcostestimate, or somewhere close.

There's one thing that I don't quite understand, and that's how
btcost_correlation() adjusts correlation for multicolumn indexes:

if (index->nkeycolumns > 1)
indexCorrelation = varCorrelation * 0.75;

That seems fine for a two-column index, I guess. But shouldn't it
compound for indexes with more keys? I mean, 0.75 * 0.75 for third
column, etc? I don't think btcostestimate() does that, it just remembers
whatever btcost_correlation() returns.

I don't know either. In general I'm out of my comfort zone here.

Don't we do something similar elsewhere? For example, IIRC we do some
adjustments when estimating grouping in estimate_num_groups(), and
incremental sort had to deal with something similar too. Maybe we could
learn something from those places ... (both from the good and bad
experiences).

The only alternative approach I can think of is not to adjust the
costing for the index scan at all, and only use this to enable (or not
enable) the skipscan internally. That would mean the overall plan
remains the same, and maybe sometimes we would think an index scan would
be too expensive and use something else. Not great, but it doesn't have
the risk of regressions - IIUC we can disable the skipscan at runtime,
if we realize it's not really helpful.

In general I would greatly prefer to not have a distinct kind of index
path for scans that use skip scan. I'm quite keen on a design that
allows the scan to adapt to unpredictable conditions at runtime.

Right. I don't think I've been suggesting having a separate path, I 100%
agree it's better to have this as an option for index scan paths.

Of course, that doesn't preclude passing the index scan a hint about
what's likely to work at runtime, based on information figured out
when costing the scan. Perhaps that will prove necessary to avoid
regressing index scans that are naturally quite cheap already -- scans
where we really need to have the right general idea from the start to
avoid any regressions. I'm not opposed to that, provided the index
scan has the ability to change its mind when (for whatever reason) the
guidance from the optimizer turns out to be wrong.

+1 (assuming it's feasible, given the amount of available information)

As usual, I wrote a bash script to do a bit of stress testing. It
generates tables with random data, and then runs random queries with
random predicates on them, while mutating a couple parameters (like
number of workers) to trigger different plans. It does that on 16,
master and with the skipscan patch (with the fix for parallel scans).

I wonder if some of the regressions you see can be tied to the use of
an LWLock in place of the existing use of a spin lock. I did that
because I sometimes need to allocate memory to deserialize the array
keys, with the exclusive lock held. It might be the case that a lot of
these regressions are tied to that, or something else that is far from
obvious...have to investigate.

In general, I haven't done much on parallel index scans here (I only
added support for them very recently), whereas your testing places a
lot of emphasis on parallel scans. Nothing wrong with that emphasis
(it caught that 17 bug), but just want to put it in context.

Sure. With this kind of testing I don't know what I'm looking for, so I
try to cover very wide range of cases. Inevitably, some of the cases
will not test the exact subject of the patch. I think it's fine.

I've uploaded the script and results from the last run here:

https://github.com/tvondra/pg-skip-scan-tests

There's the "run-mdam.sh" script that generates tables/queries, runs
them, collects all kinds of info about the query, and produces files
with explain plans, CSV with timings, etc.

It'll take me a while to investigate all this data.

I think it'd help if I go through the results and try to prepare some
reproducers, to make it easier for you. After all, it's my script and
you'd have to reverse engineer some of it.

Anyway, I ran a couple thousand such queries, and I haven't found any
incorrect results (the script compares that between versions too). So
that's good ;-)

That's good!

But my main goal was to see how this affects performance. The tables
were pretty small (just 1M rows, maybe ~80MB), but with restarts and
dropping caches, large enough to test this.

The really compelling cases all tend to involve fairly selective index
scans. Obviously, skip scan can only save work by navigating the index
structure more efficiently (unlike loose index scan). So if the main
cost is inherently bound to be the cost of heap accesses, then we
shouldn't expect a big speed up.

For example, one of the slowed down queries is query 702 (top of page 8
in the PDF). The query is pretty simple:

explain (analyze, timing off, buffers off)
select id1,id2 from t_1000000_1000_1_2
where NOT (id1 in (:list)) AND (id2 = :value);

and it was executed on a table with random data in two columns, each
with 1000 distinct values. This is perfectly random data, so a great
match for the assumptions in costing etc.

But with uncached data, this runs in ~50 ms on master, but takes almost
200 ms with skipscan (these timings are from my laptop, but similar to
the results).

I'll need to investigate this specifically. That does seem odd.

FWIW, it's a pity that the patch doesn't know how to push down the NOT
IN () here. The MDAM paper contemplates such a scheme. We see the use
of filter quals here, when in principle this could work by using a
skip array that doesn't generate elements that appear in the NOT IN()
list (it'd generate every possible indexable value *except* the given
list/array values). The only reason that I haven't implemented this
yet is because I'm not at all sure how to make it work on the
optimizer side. The nbtree side of the implementation will probably be
quite straightforward, since it's really just a slight variant of a
skip array, that excludes certain values.

-- with skipscan
Index Only Scan using t_1000000_1000_1_2_id1_id2_idx on
t_1000000_1000_1_2 (cost=0.96..983.26 rows=1719 width=16)
(actual rows=811 loops=1)
Index Cond: (id2 = 997)
Index Searches: 1007
Filter: (id1 <> ALL ('{983,...,640}'::bigint[]))
Rows Removed by Filter: 163
Heap Fetches: 0
Planning Time: 3.730 ms
Execution Time: 238.554 ms
(8 rows)

I haven't looked into why this is happening, but this seems like a
pretty good match for skipscan (on the first column). And for the
costing too - it's perfectly random data, no correllation, etc.

I wonder what "Buffers: N" shows? That's usually the first thing I
look at (that and "Index Searches", which looks like what you said it
should look like here). But, yeah, let me get back to you on this.

Yeah, I forgot to get that from my reproducer. But the logs in the
github repo with results has BUFFERS - for master (SEQ 12621), the plan
looks like this:

Index Only Scan using t_1000000_1000_1_2_id1_id2_idx
on t_1000000_1000_1_2
(cost=0.96..12179.41 rows=785 width=16)
(actual rows=785 loops=1)
Index Cond: (id2 = 997)
Filter: (id1 <> ALL ('{983, ..., 640}'::bigint[]))
Rows Removed by Filter: 181
Heap Fetches: 0
Buffers: shared read=3094
Planning:
Buffers: shared hit=93 read=27
Planning Time: 9.962 ms
Execution Time: 38.007 ms
(10 rows)

and with the patch (SEQ 12623) it's this:

Index Only Scan using t_1000000_1000_1_2_id1_id2_idx
on t_1000000_1000_1_2
(cost=0.96..1745.27 rows=784 width=16)
(actual rows=785 loops=1)
Index Cond: (id2 = 997)
Index Searches: 1002
Filter: (id1 <> ALL ('{983, ..., 640}'::bigint[]))
Rows Removed by Filter: 181
Heap Fetches: 0
Buffers: shared hit=1993 read=1029
Planning:
Buffers: shared hit=93 read=27
Planning Time: 9.506 ms
Execution Time: 179.048 ms
(11 rows)

This is on exactly the same data, after dropping caches and restarting
the instance. So there should be no caching effects. Yet, there's a
pretty clear difference - the total number of buffers is the same, but
the patched version has many more hits. Yet it's slower. Weird, right?

regards

--
Tomas Vondra

In reply to: Tomas Vondra (#28)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Sep 18, 2024 at 7:36 AM Tomas Vondra <tomas@vondra.me> wrote:

Makes sense. I started with the testing before before even looking at
the code, so it's mostly a "black box" approach. I did read the 1995
paper before that, and the script generates queries with clauses
inspired by that paper, in particular:

I think that this approach with black box testing is helpful, but also
something to refine over time. Gray box testing might work best.

OK, understood. If it's essentially an independent issue (perhaps even
counts as a bug?) what about correcting it on master first? Doesn't
sound like something we'd backpatch, I guess.

What about backpatching it to 17?

As things stand, you can get quite contradictory counts of the number
of index scans due to irrelevant implementation details from parallel
index scan. It just looks wrong, particularly on 17, where it is
reasonable to expect near exact consistency between parallel and
serial scans of the same index.

Seems like a bit of a mess. IMHO we should either divide everything by
nloops (so that everything is "per loop", or not divide anything. My
vote would be to divide, but that's mostly my "learned assumption" from
the other fields. But having a 50:50 split is confusing for everyone.

My idea was that it made most sense to follow the example of
"Buffers:", since both describe physical costs.

Honestly, I'm more than ready to take whatever the path of least
resistance is. If dividing by nloops is what people want, I have no
objections.

It's worth having skip support (the idea comes from the MDAM paper),
but it's not essential. Whether or not an opclass has skip support
isn't accounted for by the cost model, but I doubt that it's worth
addressing (the cost model is already pessimistic).

I admit I'm a bit confused. I probably need to reread the paper, but my
impression was that the increment/decrement is required for skipscan to
work. If we can't do that, how would it generate the intermediate values
to search for? I imagine it would be possible to "step through" the
index, but I thought the point of skip scan is to not do that.

I think that you're probably still a bit confused because the
terminology in this area is a little confusing. There are two ways of
explaining the situation with types like text and numeric (types that
lack skip support). The two explanations might seem to be
contradictory, but they're really not, if you think about it.

The first way of explaining it, which focuses on how the scan moves
through the index:

For a text index column "a", and an int index column "b", skip scan
will work like this for a query with a qual "WHERE b = 55":

1. Find the first/lowest sorting "a" value in the index. Let's say
that it's "Aardvark".

2. Look for matches "WHERE a = 'Aardvark' and b = 55", possibly
returning some matches.

3. Find the next value after "Aardvark" in the index using a probe
like the one we'd use for a qual "WHERE a > 'Aardvark'". Let's say
that it turns out to be "Abacus".

4. Look for matches "WHERE a = 'Abacus' and b = 55"...

... (repeat these steps until we've exhaustively processed every
existing "a" value in the index)...

The second way of explaining it, which focuses on how the skip arrays
advance. Same query (and really the same behavior) as in the first
explanation:

1. Skip array's initial value is the sentinel -inf, which cannot
possibly match any real index tuple, but can still guide the search.
So we search for tuples "WHERE a = -inf AND b = 55" (actually we don't
include the "b = 55" part, since it is unnecessary, but conceptually
it's a part of what we search for within _bt_first).

2. Find that the index has no "a" values matching -inf (it inevitably
cannot have any matches for -inf), but we do locate the next highest
match. The closest matching value is "Aardvark". The skip array on "a"
therefore advances from -inf to "Aardvark".

3. Look for matches "WHERE a = 'Aardvark' and b = 55", possibly
returning some matches.

4. Reach tuples after the last match for "WHERE a = 'Aardvark' and b =
55", which will cause us to advance the array on "a" incrementally
inside _bt_advance_array_keys (just like it would if there was a
standard SAOP array on "a" instead). The skip array on "a" therefore
advances from "Aardvark" to "Aardvark" +infinitesimal (we need to use
sentinel values for this text column, which lacks skip support).

5. Look for matches "WHERE a = 'Aardvark'+infinitesimal and b = 55",
which cannot possibly find matches, but, again, can reposition the
scan as needed. We can't find an exact match, of course, but we do
locate the next closest match -- which is "Abacus", again. So the skip
array now advances from "Aardvark" +infinitesimal to "Abacus". The
sentinel values are made up values, but that doesn't change anything.
(And, again, we don't include the "b = 55" part here, for the same
reason as before.)

6. Look for matches "WHERE a = 'Abacus' and b = 55"...

...(repeat these steps as many times as required)...

In summary:

Even index columns that lack skip support get to "increment" (or
"decrement") their arrays by using sentinel values that represent -inf
(or +inf for backwards scans), as well as sentinels that represent
concepts such as "Aardvark" +infinitesimal (or "Zebra" -infinitesimal
for backwards scans, say). This scheme sounds contradictory, because
in one sense it allows every skip array to be incremented, but in
another sense it makes it okay that we don't have a type-specific way
to increment values for many individual types/opclasses.

Inventing these sentinel values allows _bt_advance_array_keys to reason about
arrays without really having to care about which kinds of arrays are
involved, their order relative to each other, etc. In a certain sense,
we don't really need explicit "next key" probes of the kind that the
MDAM paper contemplates, though we do still require the same index
accesses as a design with explicit accesses.

Does that make sense?

Obviously, if we did add skip support for text, it would be very
unlikely to help performance. Sure, one can imagine incrementing from
"Aardvark" to "Aardvarl" using dedicated opclass infrastructure, but
that isn't very helpful. You're almost certain to end up accessing the
same pages with such a scheme, anyway. What are the chances of an
index with a leading text column actually containing tuples matching
(say) "WHERE a = 'Aardvarl' and b = 55"? The chances are practically
zero. Whereas if the column "a" happens to use a discrete type such as
integer or date, then skip support is likely to help: there's a decent
chance that a value generated by incrementing the last value
(and I mean incrementing it for real) will find a real match when
combined with the user-supplied "b" predicate.

It might be possible to add skip support for text, but there wouldn't
be much point.

Anyway, probably a good idea for extending the stress testing script.
Right now it tests with "bigint" columns only.

Good idea.

Hmmm, yeah. I think it'd be useful to explain this reasoning (assuming
no correlation means pessimistic skipscan costing) in a comment before
btcostestimate, or somewhere close.

Will do.

Don't we do something similar elsewhere? For example, IIRC we do some
adjustments when estimating grouping in estimate_num_groups(), and
incremental sort had to deal with something similar too. Maybe we could
learn something from those places ... (both from the good and bad
experiences).

I'll make a note of that. Gonna focus on regressions for now.

Right. I don't think I've been suggesting having a separate path, I 100%
agree it's better to have this as an option for index scan paths.

Cool.

Sure. With this kind of testing I don't know what I'm looking for, so I
try to cover very wide range of cases. Inevitably, some of the cases
will not test the exact subject of the patch. I think it's fine.

I agree. Just wanted to make sure that we were on the same page.

I think it'd help if I go through the results and try to prepare some
reproducers, to make it easier for you. After all, it's my script and
you'd have to reverse engineer some of it.

Yes, that would be helpful.

I'll probably memorialize the problem by writing my own minimal test
case for it. I'm using the same TDD approach for this project as was
used for the related Postgres 17 project.

This is on exactly the same data, after dropping caches and restarting
the instance. So there should be no caching effects. Yet, there's a
pretty clear difference - the total number of buffers is the same, but
the patched version has many more hits. Yet it's slower. Weird, right?

Yes, it's weird. It seems likely that you've found an unambiguous bug,
not just a "regular" performance regression. The regressions that I
already know about aren't nearly this bad. So it seems like you have
the right general idea about what to expect, and it seems like your
approach to testing the patch is effective.

--
Peter Geoghegan

In reply to: Tomas Vondra (#24)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Mon, Sep 16, 2024 at 6:05 PM Tomas Vondra <tomas@vondra.me> wrote:

For example, one of the slowed down queries is query 702 (top of page 8
in the PDF). The query is pretty simple:

explain (analyze, timing off, buffers off)
select id1,id2 from t_1000000_1000_1_2
where NOT (id1 in (:list)) AND (id2 = :value);

and it was executed on a table with random data in two columns, each
with 1000 distinct values.

I cannot recreate this problem using the q702.sql repro you provided.
Feels like I'm missing a step, because I find that skip scan wins
nicely here.

This is perfectly random data, so a great
match for the assumptions in costing etc.

FWIW, I wouldn't say that this is a particularly sympathetic case for
skip scan. It's definitely still a win, but less than other cases I
can imagine. This is due to the relatively large number of rows
returned by the scan. Plus 1000 distinct leading values for a skip
array isn't all that low, so we end up scanning over 1/3 of all of the
leaf pages in the index.

BTW, be careful to distinguish between leaf pages and internal pages
when interpreting "Buffers:" output with the patch. Generally
speaking, the patch repeats many internal page accesses, which needs
to be taken into account when compare "Buffers:" counts against
master. It's not uncommon for 3/4 or even 4/5 of all index page hits
to be for internal pages with the patch. Whereas on master the number
of internal page hits is usually tiny. This is one reason why the
additional context provided by "Index Searches:" can be helpful.

But with uncached data, this runs in ~50 ms on master, but takes almost
200 ms with skipscan (these timings are from my laptop, but similar to
the results).

Even 50ms seems really slow for your test case -- with or without my
patch applied.

Are you sure that this wasn't an assert-enabled build? There's lots of
extra assertions for the code paths used by skip scan for this, which
could explain the apparent regression.

I find that this same query takes only ~2.056 ms with the patch. When
I disabled skip scan locally via "set skipscan_prefix_cols = 0" (which
should give me behavior that's pretty well representative of master),
it takes ~12.039 ms. That's exactly what I'd expect for this query: a
solid improvement, though not the really enormous ones that you'll see
when skip scan is able to avoid reading many of the index pages that
master reads.

--
Peter Geoghegan

#31Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#30)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 9/19/24 21:22, Peter Geoghegan wrote:

On Mon, Sep 16, 2024 at 6:05 PM Tomas Vondra <tomas@vondra.me> wrote:

For example, one of the slowed down queries is query 702 (top of page 8
in the PDF). The query is pretty simple:

explain (analyze, timing off, buffers off)
select id1,id2 from t_1000000_1000_1_2
where NOT (id1 in (:list)) AND (id2 = :value);

and it was executed on a table with random data in two columns, each
with 1000 distinct values.

I cannot recreate this problem using the q702.sql repro you provided.
Feels like I'm missing a step, because I find that skip scan wins
nicely here.

I don't know, I can reproduce it just fine. I just tried with v7.

What I do is this:

1) build master and patched versions:

./configure --enable-depend --prefix=/mnt/data/builds/$(build}/
make -s clean
make -s -j4 install

2) create a new cluster (default config), create DB, generate the data

3) restart cluster, drop caches

4) run the query from the SQL script

I suspect you don't do (3). I didn't mention this explicitly, my message
only said "with uncached data", so maybe that's the problem?

This is perfectly random data, so a great
match for the assumptions in costing etc.

FWIW, I wouldn't say that this is a particularly sympathetic case for
skip scan. It's definitely still a win, but less than other cases I
can imagine. This is due to the relatively large number of rows
returned by the scan. Plus 1000 distinct leading values for a skip
array isn't all that low, so we end up scanning over 1/3 of all of the
leaf pages in the index.

I wasn't suggesting it's a sympathetic case for skipscan. My point is
that it perfectly matches the costing assumptions, i.e. columns are
independent etc. But if it's not sympathetic, maybe the cost shouldn't
be 1/5 of cost from master?

BTW, be careful to distinguish between leaf pages and internal pages
when interpreting "Buffers:" output with the patch. Generally
speaking, the patch repeats many internal page accesses, which needs
to be taken into account when compare "Buffers:" counts against
master. It's not uncommon for 3/4 or even 4/5 of all index page hits
to be for internal pages with the patch. Whereas on master the number
of internal page hits is usually tiny. This is one reason why the
additional context provided by "Index Searches:" can be helpful.

Yeah, I recall there's an issue with that.

But with uncached data, this runs in ~50 ms on master, but takes almost
200 ms with skipscan (these timings are from my laptop, but similar to
the results).

Even 50ms seems really slow for your test case -- with or without my
patch applied.

Are you sure that this wasn't an assert-enabled build? There's lots of
extra assertions for the code paths used by skip scan for this, which
could explain the apparent regression.

I find that this same query takes only ~2.056 ms with the patch. When
I disabled skip scan locally via "set skipscan_prefix_cols = 0" (which
should give me behavior that's pretty well representative of master),
it takes ~12.039 ms. That's exactly what I'd expect for this query: a
solid improvement, though not the really enormous ones that you'll see
when skip scan is able to avoid reading many of the index pages that
master reads.

I'm pretty sure you're doing this on cached data, because 2ms is exactly
the timing I see in that case.

regards

--
Tomas Vondra

#32Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#29)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 9/18/24 20:52, Peter Geoghegan wrote:

On Wed, Sep 18, 2024 at 7:36 AM Tomas Vondra <tomas@vondra.me> wrote:

Makes sense. I started with the testing before before even looking at
the code, so it's mostly a "black box" approach. I did read the 1995
paper before that, and the script generates queries with clauses
inspired by that paper, in particular:

I think that this approach with black box testing is helpful, but also
something to refine over time. Gray box testing might work best.

OK, understood. If it's essentially an independent issue (perhaps even
counts as a bug?) what about correcting it on master first? Doesn't
sound like something we'd backpatch, I guess.

What about backpatching it to 17?

As things stand, you can get quite contradictory counts of the number
of index scans due to irrelevant implementation details from parallel
index scan. It just looks wrong, particularly on 17, where it is
reasonable to expect near exact consistency between parallel and
serial scans of the same index.

Yes, I think backpatching to 17 would be fine. I'd be worried about
maybe disrupting some monitoring in production systems, but for 17 that
shouldn't be a problem yet. So fine with me.

FWIW I wonder how likely is it that someone has some sort of alerting
tied to this counter. I'd bet few people do. It's probably more about a
couple people looking at explain plans, but they'll be confused even if
we change that only starting with 17.

Seems like a bit of a mess. IMHO we should either divide everything by
nloops (so that everything is "per loop", or not divide anything. My
vote would be to divide, but that's mostly my "learned assumption" from
the other fields. But having a 50:50 split is confusing for everyone.

My idea was that it made most sense to follow the example of
"Buffers:", since both describe physical costs.

Honestly, I'm more than ready to take whatever the path of least
resistance is. If dividing by nloops is what people want, I have no
objections.

I don't have a strong opinion on this. I just know I'd be confused by
half the counters being total and half /loop, but chances are other
people would disagree.

It's worth having skip support (the idea comes from the MDAM paper),
but it's not essential. Whether or not an opclass has skip support
isn't accounted for by the cost model, but I doubt that it's worth
addressing (the cost model is already pessimistic).

I admit I'm a bit confused. I probably need to reread the paper, but my
impression was that the increment/decrement is required for skipscan to
work. If we can't do that, how would it generate the intermediate values
to search for? I imagine it would be possible to "step through" the
index, but I thought the point of skip scan is to not do that.

I think that you're probably still a bit confused because the
terminology in this area is a little confusing. There are two ways of
explaining the situation with types like text and numeric (types that
lack skip support). The two explanations might seem to be
contradictory, but they're really not, if you think about it.

The first way of explaining it, which focuses on how the scan moves
through the index:

For a text index column "a", and an int index column "b", skip scan
will work like this for a query with a qual "WHERE b = 55":

1. Find the first/lowest sorting "a" value in the index. Let's say
that it's "Aardvark".

2. Look for matches "WHERE a = 'Aardvark' and b = 55", possibly
returning some matches.

3. Find the next value after "Aardvark" in the index using a probe
like the one we'd use for a qual "WHERE a > 'Aardvark'". Let's say
that it turns out to be "Abacus".

4. Look for matches "WHERE a = 'Abacus' and b = 55"...

... (repeat these steps until we've exhaustively processed every
existing "a" value in the index)...

Ah, OK. So we do probe the index like this. I was under the impression
we don't do that. But yeah, this makes sense.

The second way of explaining it, which focuses on how the skip arrays
advance. Same query (and really the same behavior) as in the first
explanation:

1. Skip array's initial value is the sentinel -inf, which cannot
possibly match any real index tuple, but can still guide the search.
So we search for tuples "WHERE a = -inf AND b = 55" (actually we don't
include the "b = 55" part, since it is unnecessary, but conceptually
it's a part of what we search for within _bt_first).

2. Find that the index has no "a" values matching -inf (it inevitably
cannot have any matches for -inf), but we do locate the next highest
match. The closest matching value is "Aardvark". The skip array on "a"
therefore advances from -inf to "Aardvark".

3. Look for matches "WHERE a = 'Aardvark' and b = 55", possibly
returning some matches.

4. Reach tuples after the last match for "WHERE a = 'Aardvark' and b =
55", which will cause us to advance the array on "a" incrementally
inside _bt_advance_array_keys (just like it would if there was a
standard SAOP array on "a" instead). The skip array on "a" therefore
advances from "Aardvark" to "Aardvark" +infinitesimal (we need to use
sentinel values for this text column, which lacks skip support).

5. Look for matches "WHERE a = 'Aardvark'+infinitesimal and b = 55",
which cannot possibly find matches, but, again, can reposition the
scan as needed. We can't find an exact match, of course, but we do
locate the next closest match -- which is "Abacus", again. So the skip
array now advances from "Aardvark" +infinitesimal to "Abacus". The
sentinel values are made up values, but that doesn't change anything.
(And, again, we don't include the "b = 55" part here, for the same
reason as before.)

6. Look for matches "WHERE a = 'Abacus' and b = 55"...

...(repeat these steps as many times as required)...

Yeah, this makes more sense. Thanks.

In summary:

Even index columns that lack skip support get to "increment" (or
"decrement") their arrays by using sentinel values that represent -inf
(or +inf for backwards scans), as well as sentinels that represent
concepts such as "Aardvark" +infinitesimal (or "Zebra" -infinitesimal
for backwards scans, say). This scheme sounds contradictory, because
in one sense it allows every skip array to be incremented, but in
another sense it makes it okay that we don't have a type-specific way
to increment values for many individual types/opclasses.

Inventing these sentinel values allows _bt_advance_array_keys to reason about
arrays without really having to care about which kinds of arrays are
involved, their order relative to each other, etc. In a certain sense,
we don't really need explicit "next key" probes of the kind that the
MDAM paper contemplates, though we do still require the same index
accesses as a design with explicit accesses.

Does that make sense?

Yes, it does. Most of my confusion was caused by my belief that we can't
probe the index for the next value without "incrementing" the current
value, but that was a silly idea.

Obviously, if we did add skip support for text, it would be very
unlikely to help performance. Sure, one can imagine incrementing from
"Aardvark" to "Aardvarl" using dedicated opclass infrastructure, but
that isn't very helpful. You're almost certain to end up accessing the
same pages with such a scheme, anyway. What are the chances of an
index with a leading text column actually containing tuples matching
(say) "WHERE a = 'Aardvarl' and b = 55"? The chances are practically
zero. Whereas if the column "a" happens to use a discrete type such as
integer or date, then skip support is likely to help: there's a decent
chance that a value generated by incrementing the last value
(and I mean incrementing it for real) will find a real match when
combined with the user-supplied "b" predicate.

It might be possible to add skip support for text, but there wouldn't
be much point.

Stupid question - so why does it make sense for types like int? There
can also be a lot of values between the current and the next value, so
why would that be very different from "incrementing" a text value?

I think it'd help if I go through the results and try to prepare some
reproducers, to make it easier for you. After all, it's my script and
you'd have to reverse engineer some of it.

Yes, that would be helpful.

I'll probably memorialize the problem by writing my own minimal test
case for it. I'm using the same TDD approach for this project as was
used for the related Postgres 17 project.

Sure. Still, giving you a reproducer should make it easier .

This is on exactly the same data, after dropping caches and restarting
the instance. So there should be no caching effects. Yet, there's a
pretty clear difference - the total number of buffers is the same, but
the patched version has many more hits. Yet it's slower. Weird, right?

Yes, it's weird. It seems likely that you've found an unambiguous bug,
not just a "regular" performance regression. The regressions that I
already know about aren't nearly this bad. So it seems like you have
the right general idea about what to expect, and it seems like your
approach to testing the patch is effective.

Yeah, it's funny. It's not the first time I start stress testing a patch
only to stumble over some pre-existing issues ... ;-)

regards

--
Tomas Vondra

In reply to: Tomas Vondra (#31)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Sep 20, 2024 at 9:45 AM Tomas Vondra <tomas@vondra.me> wrote:

3) restart cluster, drop caches

4) run the query from the SQL script

I suspect you don't do (3). I didn't mention this explicitly, my message
only said "with uncached data", so maybe that's the problem?

You're right that I didn't do step 3 here. I'm generally in the habit
of using fully cached data when testing this kind of work.

The only explanation I can think of is that (at least on your
hardware) OS readahead helps the master branch more than skipping
helps the patch. That's surprising, but I guess it's possible here
because skip scan only needs to access about every third page. And
because this particular index was generated by CREATE INDEX, and so
happens to have a strong correlation between key space order and
physical block order. And probably because this is an index-only scan.

I wasn't suggesting it's a sympathetic case for skipscan. My point is
that it perfectly matches the costing assumptions, i.e. columns are
independent etc. But if it's not sympathetic, maybe the cost shouldn't
be 1/5 of cost from master?

The costing is pretty accurate if we assume cached data, though --
which is what the planner will actually assume. In any case, is that
really the only problem you see here? That the costing might be
inaccurate because it fails to account for some underlying effect,
such as the influence of OS readhead?

Let's assume for a moment that the regression is indeed due to
readahead effects, and that we deem it to be unacceptable. What can be
done about it? I have a really hard time thinking of a fix, since by
most conventional measures skip scan is indeed much faster here.

--
Peter Geoghegan

#34Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#33)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 9/20/24 16:21, Peter Geoghegan wrote:

On Fri, Sep 20, 2024 at 9:45 AM Tomas Vondra <tomas@vondra.me> wrote:

3) restart cluster, drop caches

4) run the query from the SQL script

I suspect you don't do (3). I didn't mention this explicitly, my message
only said "with uncached data", so maybe that's the problem?

You're right that I didn't do step 3 here. I'm generally in the habit
of using fully cached data when testing this kind of work.

The only explanation I can think of is that (at least on your
hardware) OS readahead helps the master branch more than skipping
helps the patch. That's surprising, but I guess it's possible here
because skip scan only needs to access about every third page. And
because this particular index was generated by CREATE INDEX, and so
happens to have a strong correlation between key space order and
physical block order. And probably because this is an index-only scan.

Good idea. Yes, it does seem to be due to readahead - if I disable that,
the query takes ~320ms on master and ~280ms with the patch.

I wasn't suggesting it's a sympathetic case for skipscan. My point is
that it perfectly matches the costing assumptions, i.e. columns are
independent etc. But if it's not sympathetic, maybe the cost shouldn't
be 1/5 of cost from master?

The costing is pretty accurate if we assume cached data, though --
which is what the planner will actually assume. In any case, is that
really the only problem you see here? That the costing might be
inaccurate because it fails to account for some underlying effect,
such as the influence of OS readhead?

Let's assume for a moment that the regression is indeed due to
readahead effects, and that we deem it to be unacceptable. What can be
done about it? I have a really hard time thinking of a fix, since by
most conventional measures skip scan is indeed much faster here.

It does seem to be due to readahead, and the costing not accounting for
these effects. And I don't think it's unacceptable - I don't think we
consider readahead elsewhere, and it certainly is not something I'd
expect this patch to fix. So I think it's fine.

Ultimately, I think this should be "fixed" by explicitly prefetching
pages. My index prefetching patch won't really help, because AFAIK this
is about index pages. And I don't know how feasible it is.

regards

--
Tomas Vondra

In reply to: Tomas Vondra (#32)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Sep 20, 2024 at 10:07 AM Tomas Vondra <tomas@vondra.me> wrote:

Yes, I think backpatching to 17 would be fine. I'd be worried about
maybe disrupting some monitoring in production systems, but for 17 that
shouldn't be a problem yet. So fine with me.

I'll commit minimal changes to _bt_first that at least make the
counters consistent, then. I'll do so soon.

FWIW I wonder how likely is it that someone has some sort of alerting
tied to this counter. I'd bet few people do. It's probably more about a
couple people looking at explain plans, but they'll be confused even if
we change that only starting with 17.

On 17 the behavior in this area is totally different, either way.

Ah, OK. So we do probe the index like this. I was under the impression
we don't do that. But yeah, this makes sense.

Well, we don't have *explicit* next-key probes. If you think of values
like "Aardvark" + infinitesimal as just another array value (albeit
one that requires a little special handling in _bt_first), then there
are no explicit probes. There are no true special cases required.

Maybe this sounds like a very academic point. I don't think that it
is, though. Bear in mind that even when _bt_first searches for index
tuples matching a value like "Aardvark" + infinitesimal, there's some
chance that _bt_search will return a leaf page with tuples that the
index scan ultimately returns. And so there really is no "separate
explicit probe" of the kind the MDAM paper contemplates.

When this happens, we won't get any exact matches for the sentinel
search value, but there could still be matches for (say) "WHERE a =
'Abacus' AND b = 55" on that same leaf page. In general, repositioning
the scan to later "within" the 'Abacus' index tuples might not be
required -- our initial position (based on the sentinel search key)
could be "close enough". This outcome is more likely to happen if the
query happened to be written "WHERE b = 1", rather than "WHERE b =
55".

Yes, it does. Most of my confusion was caused by my belief that we can't
probe the index for the next value without "incrementing" the current
value, but that was a silly idea.

It's not a silly idea, I think. Technically that understanding is
fairly accurate -- we often *do* have to "increment" to get to the
next value (though reading the next value from an index tuple and then
repositioning using it with later/less significant scan keys is the
other possibility).

Incrementing is always possible, even without skip support, because we
can always fall back on +infinitesimal style sentinel values (AKA
SK_BT_NEXTPRIOR values). That's the definitional sleight-of-hand that
allows _bt_advance_array_keys to not have to think about skip arrays
as a special case, regardless of whether or not they happen to have
skip support.

It might be possible to add skip support for text, but there wouldn't
be much point.

Stupid question - so why does it make sense for types like int? There
can also be a lot of values between the current and the next value, so
why would that be very different from "incrementing" a text value?

Not a stupid question at all. You're right; it'd be the same.

Obviously, there are at least some workloads (probably most) where any
int columns will contain values that are more or less fully
contiguous. I also expect there to be some workloads where int columns
appear in B-Tree indexes that contain values with large gaps between
neighboring values (e.g., because the integers are hash values). We'll
always use skip support for any omitted prefix int column (same with
any opclass that offers skip support), but we can only expect to see a
benefit in the former "dense" cases -- never in the latter "sparse"
cases.

The MDAM paper talks about an adaptive strategy for dense columns and
sparse columns. I don't see any point in that, and assume that it's
down to some kind of implementation deficiencies in NonStop SQL back
in the 1990s. I can just always use skip support in the hope that
integer column data will turn out to be "sparse" because there's no
downside to being optimistic about it. The access patterns are exactly
the same as they'd be with skip support disabled.

My "academic point" about not having *explicit* next-key probes might
make more sense now. This is the thing that makes it okay to always be
optimistic about types with skip support containing "dense" data.

FWIW I actually have skip support for the UUID opclass. I implemented
it to have test coverage for pass-by-reference types in certain code
paths, but it's otherwise I don't expect it to be useful -- in
practice all UUID columns contain "sparse" data. There's still no real
downside to it, though. (I wouldn't try to do it with text because
it'd be much harder to implement skip support correctly, especially
with collated text.)

--
Peter Geoghegan

In reply to: Peter Geoghegan (#35)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Sep 20, 2024 at 10:59 AM Peter Geoghegan <pg@bowt.ie> wrote:

On Fri, Sep 20, 2024 at 10:07 AM Tomas Vondra <tomas@vondra.me> wrote:

Yes, I think backpatching to 17 would be fine. I'd be worried about
maybe disrupting some monitoring in production systems, but for 17 that
shouldn't be a problem yet. So fine with me.

I'll commit minimal changes to _bt_first that at least make the
counters consistent, then. I'll do so soon.

Pushed, thanks

--
Peter Geoghegan

In reply to: Tomas Vondra (#28)
3 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Sep 18, 2024 at 7:36 AM Tomas Vondra <tomas@vondra.me> wrote:

3) v6-0003-Refactor-handling-of-nbtree-array-redundancies.patch

- nothing

Great. I think that I should be able to commit this one soon, since
it's independently useful work.

+1

I pushed this just now. There was one small change: I decided that it
made more sense to repalloc() in the case where skip scan must enlarge
the so.keyData[] space, rather than doing the so.keyData[] allocation
lazily in all cases. I was concerned that my original approach might
regress nested loop joins with very fast inner index scans.

Attached is v8. I still haven't worked through any of your feedback,
Tomas. Again, this revision is just to keep CFBot happy by fixing the
bit rot on the master branch created by my recent commits.

--
Peter Geoghegan

Attachments:

v8-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v8-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From 7e3050c3442c6d553c199be2046241dd80f362b7 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v8 1/3] Show index search count in EXPLAIN ANALYZE.

Also stop counting the case where nbtree detects contradictory quals as
a distinct index search (do so neither in EXPLAIN ANALYZE nor in the
pg_stat_*_indexes.idx_scan stats).

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |  3 +
 src/backend/access/brin/brin.c                |  1 +
 src/backend/access/gin/ginscan.c              |  1 +
 src/backend/access/gist/gistget.c             |  2 +
 src/backend/access/hash/hashsearch.c          |  1 +
 src/backend/access/index/genam.c              |  1 +
 src/backend/access/nbtree/nbtree.c            | 11 ++++
 src/backend/access/nbtree/nbtsearch.c         |  1 +
 src/backend/access/spgist/spgscan.c           |  1 +
 src/backend/commands/explain.c                | 38 +++++++++++++
 doc/src/sgml/bloom.sgml                       |  6 +-
 doc/src/sgml/monitoring.sgml                  | 12 +++-
 doc/src/sgml/perform.sgml                     |  8 +++
 doc/src/sgml/ref/explain.sgml                 |  3 +-
 doc/src/sgml/rules.sgml                       |  1 +
 src/test/regress/expected/brin_multi.out      | 27 ++++++---
 src/test/regress/expected/memoize.out         | 50 +++++++++++-----
 src/test/regress/expected/partition_prune.out | 57 ++++++++++++++-----
 src/test/regress/expected/select.out          |  3 +-
 src/test/regress/sql/memoize.sql              |  6 +-
 src/test/regress/sql/partition_prune.sql      |  4 ++
 21 files changed, 192 insertions(+), 45 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 114a85dc4..361c33fca 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -131,6 +131,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 60853a0f6..879d5589d 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -582,6 +582,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index f2fd62afb..5e423e155 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index b35b8a975..36f1435cb 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index 0d99d6abc..927ba1039 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 4ec43e3c0..466d766b0 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -116,6 +116,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 56e502c4f..b413433d9 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -70,6 +70,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* instrumentation */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -550,6 +551,7 @@ btinitparallelscan(void *target)
 	SpinLockInit(&bt_target->btps_mutex);
 	bt_target->btps_scanPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -575,6 +577,7 @@ btparallelrescan(IndexScanDesc scan)
 	SpinLockAcquire(&btscan->btps_mutex);
 	btscan->btps_scanPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -683,6 +686,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			*pageno = btscan->btps_scanPage;
 			exit_loop = true;
@@ -764,6 +772,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -797,6 +807,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 	{
 		btscan->btps_scanPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index fff7c89ea..b11112539 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -963,6 +963,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 301786185..be668abf2 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index aaec43989..6f6d5a8c2 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -89,6 +90,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -1989,6 +1991,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			if (es->analyze)
+				show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2002,6 +2006,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			if (es->analyze)
+				show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2018,6 +2024,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			if (es->analyze)
+				show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2528,6 +2536,36 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc && scanDesc->nsearches > 0)
+		ExplainPropertyUInteger("Index Searches", NULL,
+								scanDesc->nsearches, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 19f2b172c..92b13f539 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -170,9 +170,10 @@ CREATE INDEX
    Heap Blocks: exact=28
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..1792.00 rows=2 width=0) (actual time=0.356..0.356 rows=29 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 0.408 ms
-(8 rows)
+(9 rows)
 </programlisting>
   </para>
 
@@ -202,11 +203,12 @@ CREATE INDEX
    -&gt;  BitmapAnd  (cost=24.34..24.34 rows=2 width=0) (actual time=0.027..0.027 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..12.04 rows=500 width=0) (actual time=0.026..0.026 rows=0 loops=1)
                Index Cond: (i5 = 123451)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
-(9 rows)
+(10 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index a2fda4677..6b21bb85f 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4153,12 +4153,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index ff689b652..1f2172960 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -702,8 +702,10 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Heap Blocks: exact=10
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 1
  Planning Time: 0.485 ms
  Execution Time: 0.073 ms
 </screen>
@@ -754,6 +756,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Heap Blocks: exact=90
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
  Planning Time: 0.187 ms
  Execution Time: 3.036 ms
 </screen>
@@ -819,6 +822,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -848,9 +852,11 @@ EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique
          Buffers: shared hit=4 read=3
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared hit=2
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
                Buffers: shared hit=2 read=3
  Planning:
    Buffers: shared hit=3
@@ -883,6 +889,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Heap Blocks: exact=90
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1017,6 +1024,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
  Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
  Planning Time: 0.077 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a854..e042638b7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -502,9 +502,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    Batches: 1  Memory Usage: 24kB
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(7 rows)
+(8 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index ae9ce9d8e..c24d56007 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 9ee09fe2f..1448179fb 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -312,7 +325,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(10 rows)
+               Index Searches: N
+(11 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -329,7 +343,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(10 rows)
+               Index Searches: N
+(11 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -349,6 +364,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -356,9 +372,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -366,8 +384,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -379,6 +398,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -387,11 +407,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7a03b4e36..18ea272b6 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2692,12 +2696,13 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+(53 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off)
@@ -2713,6 +2718,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
@@ -2741,7 +2747,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(38 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off)
@@ -2757,6 +2763,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
@@ -2787,7 +2794,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(40 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2865,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2885,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,8 +2974,10 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
@@ -2971,7 +2986,7 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
                Index Cond: (col1 < tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+(17 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2984,6 +2999,7 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
@@ -2992,7 +3008,7 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+(16 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3043,20 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 5
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 4
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+(18 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3050,15 +3069,17 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 3
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+(17 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3122,7 +3143,8 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
                Index Cond: (col1 > tbl1.col1)
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(16 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3482,12 +3504,14 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(15);
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3527,10 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(25);
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3578,16 @@ explain (analyze, costs off, summary off, timing off) select * from ma_test wher
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(17 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4157,17 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(18 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 33a6dceb0..b7cf35b9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index 2eaeb1477..9afe205e0 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 442428d93..085e746af 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

v8-0003-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v8-0003-Add-skip-scan-to-nbtree.patchDownload
From 33505adc75999c15c01d84dfb7330f0245746efc Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v8 3/3] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
useful in cases where the total number of distinct values in the column
'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.

Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXTPRIOR scan key flag, without directly changing its sk_argument.
The presence of NEXTPRIOR makes the scan interpret the key's sk_argument
as coming immediately after (or coming immediately before) sk_argument
in the key space.  The key value must still come before (or still come
after) any possible greater-than (or less-than) indexable/non-sentinel
value.  Obviously, the scan will never locate any exactly equal tuples.
But attempting to locate a match serves to make the scan locate the true
next value in whatever way it determines is most efficient, without any
need for special cases in high level scan-related code.  In particular,
this design obviates the need for explicit "next key" index probes.

Though it's typical for nbtree preprocessing to cons up skip arrays when
it will allow the scan to apply one or more omitted-from-query leading
key columns when skipping, that's never a requirement.  There are hardly
any limitations around where skip arrays/scan keys may appear relative
to conventional/input scan keys.  This is no less true in the presence
of conventional SAOP array scan keys, which may both roll over and be
rolled over by skip arrays.  For example, a skip array on the column "b"
is generated with quals such as "WHERE a = 42 AND c IN (1, 2, 3)".  As
with any nbtree scan involving arrays, whether or not we actually skip
depends on the physical characteristics of the index during the scan.

The optimizer doesn't use distinct new index paths to represent index
skip scans.  Skipping isn't an either/or question.  It's possible for
individual index scans to conspicuously vary how and when they skip in
order to deal with variation in how leading column values cluster
together over the key space of the index.  A dynamic strategy seems to
work best.  Skipping can be used during nbtree bitmap index scans,
nbtree index scans, and nbtree index-only scans.  Parallel index skip
scan is also supported.

Preprocessing will never cons up a skip array for an index column that
has an equality strategy scan key on input, but will do so for indexed
columns that are only constrained by inequality type input scan keys.
This allows the scan to skip using a range skip array.  These are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".
Such transformations only happen when they enable later preprocessing to
mark the copied-from-input scan key on "b" required to continue the scan
(otherwise, preprocessing directly outputs the >= and <= keys on "a" in
the traditional way, without adding a superseding skip array on "a").

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   27 +-
 src/include/catalog/pg_amproc.dat             |   16 +
 src/include/catalog/pg_proc.dat               |   24 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  109 ++
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  273 ++++
 src/backend/access/nbtree/nbtree.c            |  205 ++-
 src/backend/access/nbtree/nbtsearch.c         |   93 +-
 src/backend/access/nbtree/nbtutils.c          | 1422 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   46 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  368 +++--
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/uuid.c                  |   71 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/btree.sgml                       |   13 +
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   40 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |    6 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   61 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    2 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 34 files changed, 2638 insertions(+), 309 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c51de742e..0826c124f 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -202,7 +202,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index d709fe08d..e1b6b883e 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1031,10 +1033,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard arrays that store elements in memory */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup set to valid routine? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* opclass skip scan support, when use_sksup */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo	order_low;		/* low_compare's ORDER proc */
+	FmgrInfo	order_high;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1124,6 +1138,9 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array, for skip scan */
+#define SK_BT_NEGPOSINF	0x00080000	/* no sk_argument, -inf/+inf key */
+#define SK_BT_NEXTPRIOR	0x00100000	/* sk_argument is next/prior key */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1160,6 +1177,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1170,7 +1191,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5d7fe292b..c0ccdfdfb 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -122,6 +128,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +149,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +170,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +205,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +275,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 43f608d7a..14ce6c79c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2250,6 +2265,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4437,6 +4455,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -9294,6 +9315,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d70e6d37e..5b58739ad 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..d91390fc6
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,109 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * This happens at the point where the scan determines that another primitive
+ * index scan is required.  The next value is used (in combination with at
+ * least one additional lower-order non-skip key, taken from the SQL query) to
+ * relocate the scan, skipping over many irrelevant leaf pages in the process.
+ *
+ * Skip support generally works best with discrete types such as integer,
+ * date, and boolean; types where there is a decent chance that indexes will
+ * contain contiguous values (given a leading attributes using the opclass).
+ * When gaps/discontinuities are naturally rare (e.g., a leading identity
+ * column in a composite index, a date column preceding a product_id column),
+ * then it makes sense for skip scans to optimistically assume that the next
+ * distinct indexable value will find directly matching index tuples.
+ *
+ * The B-Tree code can fall back on next-key sentinel values for any opclass
+ * that doesn't provide its own skip support function.  There is no point in
+ * providing skip support unless the next indexed key value is often the next
+ * indexable value (at least with some workloads).  Opclasses where that never
+ * works out in practice should just rely on the B-Tree AM's generic next-key
+ * fallback strategy.  Opclasses where adding skip support is infeasible or
+ * hard (e.g., an opclass for a continuous type) can also use the fallback.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called.
+ * If an opclass can only set some of the fields, then it cannot safely
+ * provide a skip support routine.
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming standard ascending
+	 * order).  This helps the B-Tree code with finding its initial position
+	 * at the leaf level (during the skip scan's first primitive index scan).
+	 * In other words, it gives the B-Tree code a useful value to start from,
+	 * before any data has been read from the index.
+	 *
+	 * low_elem and high_elem are also used by skip scans to determine when
+	 * they've reached the final possible value (in the current direction).
+	 * It's typical for the scan to run out of leaf pages before it runs out
+	 * of unscanned indexable values, but it's still useful for the scan to
+	 * have a way to recognize when it has reached the last possible value
+	 * (this saves us a useless probe that just lands on the final leaf page).
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (in the case of pass-by-reference
+	 * types).  It's not okay for these functions to leak any memory.
+	 *
+	 * Both decrement and increment callbacks are guaranteed to never be
+	 * called with a NULL "existing" arg.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is undefined, and the B-Tree
+	 * code is entitled to assume that no memory will have been allocated.
+	 *
+	 * The B-Tree skip scan caller's "existing" datum is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must be liberal
+	 * in accepting every possible representational variation within the
+	 * underlying data type.  On the other hand, opclasses are _not_ expected
+	 * to preserve any information that doesn't affect how datums are sorted
+	 * (e.g., skip support for a fixed precision numeric type isn't required
+	 * to preserve datum display scale).
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 1859be614..b4f1466d4 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..26ebbf776 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index f166c7549..fe971784b 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -32,6 +32,7 @@
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
 #include "storage/smgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -71,7 +72,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* instrumentation */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -79,11 +80,21 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The reset of the space allocated in shared memory is also used when
+	 * scans need to schedule another primitive index scan.  It holds a
+	 * flattened representation of the backend's skip array datums, if any.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -536,10 +547,155 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
 	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Assume every index attribute might require that we generate a skip scan
+	 * key
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		Form_pg_attribute attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/* Restore skip array */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+
+		/* Now that old sk_argument memory is freed, copy over sk_flags */
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument to serialize */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -550,7 +706,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_scanPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	bt_target->btps_nsearches = 0;
@@ -572,15 +729,15 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_scanPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -607,6 +764,7 @@ btparallelrescan(IndexScanDesc scan)
 bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false;
 	bool		status = true;
@@ -640,7 +798,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -655,14 +813,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				*pageno = InvalidBlockNumber;
 				exit_loop = true;
 			}
@@ -699,7 +852,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			*pageno = btscan->btps_scanPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -729,10 +882,10 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber scan_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_scanPage = scan_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -769,7 +922,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -778,7 +931,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -796,6 +949,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -805,7 +959,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_scanPage == prev_scan_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -814,14 +968,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index fbf474377..cbfe34144 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -883,7 +883,6 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	Buffer		buf;
 	BTStack		stack;
 	OffsetNumber offnum;
-	StrategyNumber strat;
 	BTScanInsertData inskey;
 	ScanKey		startKeys[INDEX_MAX_KEYS];
 	ScanKeyData notnullkeys[INDEX_MAX_KEYS];
@@ -975,7 +974,20 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * a > or < boundary or find an attribute with no boundary (which can be
 	 * thought of as the same as "> -infinity"), we can't use keys for any
 	 * attributes to its right, because it would break our simplistic notion
-	 * of what initial positioning strategy to use.
+	 * of what initial positioning strategy to use.  In practice skip scan
+	 * typically enables us to use all scan keys here, even with a set of
+	 * input keys that leave a "gap" between two index attributes (cases with
+	 * multiple gaps will even manage this without any special restrictions).
+	 *
+	 * Skip scan works by having _bt_preprocess_keys cons up = boundary keys
+	 * for any index columns that were missing a = key in scan->keyData[], the
+	 * input scan keys passed to us by the executor.  This happens for index
+	 * attributes prior to the attribute of our final input scan key.  The
+	 * underlying = keys use skip arrays.  The keys can be thought of as the
+	 * same as "col = ANY('{every possible col value}')".  Note that this
+	 * often includes the array element NULL, which the scan will treat as an
+	 * IS NULL qual (the skip array's scan key is already marked SK_SEARCHNULL
+	 * when we're called, so we need no special handling for this case here).
 	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
@@ -1050,6 +1062,47 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		{
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					/* -inf/+inf element from a skip array's scan key */
+					ScanKey		origchosen = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == chosen - so->keyData)
+							break;
+					}
+
+					/* use array's inequality key in startKeys[] */
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					Assert(!chosen ||
+						   chosen->sk_attno == origchosen->sk_attno);
+
+					if (!array->null_elem)
+					{
+						/*
+						 * The array does not include a NULL element (meaning
+						 * array advancement never generates an IS NULL qual).
+						 * We'll deduce a NOT NULL key to skip over any NULLs
+						 * when there's no usable low_compare (or no usable
+						 * high_compare, during a backwards scan).
+						 *
+						 * Note: this also handles an explicit NOT NULL key
+						 * that preprocessing folded into the skip array (it
+						 * doesn't save them in low_compare/high_compare).
+						 */
+						impliesNN = origchosen;
+					}
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
 				/*
 				 * Done looking at keys for curattr.  If we didn't find a
 				 * usable boundary key, see if we can deduce a NOT NULL key.
@@ -1083,16 +1136,42 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 				startKeys[keysz++] = chosen;
 
+				if (chosen->sk_flags & SK_BT_NEXTPRIOR)
+				{
+					/*
+					 * Next/prior key element from a skip array's scan key.
+					 * 'chosen' could be SK_ISNULL, in which case startKeys[]
+					 * positions us at the first tuple > NULL (for backwards
+					 * scans it's the first tuple < NULL instead).
+					 *
+					 * Adjust strat_total, so that our = key gets treated like
+					 * a > key (or like a < key) within _bt_search.
+					 */
+					Assert(strat_total == BTEqualStrategyNumber);
+					if (ScanDirectionIsForward(dir))
+						strat_total = BTGreaterStrategyNumber;
+					else
+						strat_total = BTLessStrategyNumber;
+
+					/*
+					 * We'll never find an exact = match for a NEXTPRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[]
+					 * (besides, doing so would confuse _bt_search, since it
+					 * isn't directly aware of NEXTPRIOR sentinel values)
+					 */
+					break;
+				}
+
 				/*
 				 * Adjust strat_total, and quit if we have stored a > or <
 				 * key.
 				 */
-				strat = chosen->sk_strategy;
-				if (strat != BTEqualStrategyNumber)
+				if (chosen->sk_strategy != BTEqualStrategyNumber)
 				{
-					strat_total = strat;
-					if (strat == BTGreaterStrategyNumber ||
-						strat == BTLessStrategyNumber)
+					strat_total = chosen->sk_strategy;
+					if (chosen->sk_strategy == BTGreaterStrategyNumber ||
+						chosen->sk_strategy == BTLessStrategyNumber)
 						break;
 				}
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 025e20967..cccb59514 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the requiredness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using opclass skip
+ * support routines.  This can be used to quantify the peformance benefit that
+ * comes from having dedicated skip support, with a given test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup set to valid routine? */
+	Oid			eq_op;			/* InvalidOid means don't skip */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -64,15 +92,38 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno,
+							BTSkipPreproc *skipatts);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
-										   Datum arrdatum, ScanKey cur);
+										   Datum arrdatum, bool arrnull,
+										   ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -256,6 +307,12 @@ _bt_freestack(BTStack stack)
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by later preprocessing on output.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -271,10 +328,13 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
+	int			numArrayKeyData = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
+				numSkipArrayKeys,
 				output_ikey = 0;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -282,11 +342,14 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
+	Assert(scan->numberOfKeys);
 
-	/* Quick check to see if there are any array keys */
+	/*
+	 * Quick check to see if there are any array keys, or any missing keys we
+	 * can generate a "skip scan" array key for ourselves
+	 */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
 		cur = &scan->keyData[i];
 		if (cur->sk_flags & SK_SEARCHARRAY)
@@ -302,6 +365,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		}
 	}
 
+	numSkipArrayKeys = _bt_decide_skipatts(scan, skipatts);
+	if (numSkipArrayKeys)
+	{
+		/* At least one skip array scan key must be added to arrayKeyData[] */
+		numArrayKeys += numSkipArrayKeys;
+		/* output scan key buffer allocation needs space for skip scan keys */
+		numArrayKeyData += numSkipArrayKeys;
+	}
+
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
@@ -320,17 +392,22 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/*
+	 * Process each array key, and generate skip arrays as needed.  Also copy
+	 * every scan->keyData[] input scan key (whether it's an array or not)
+	 * into the arrayKeyData array we'll return to our caller (barring any
+	 * array scan keys that we could eliminate early through array merging).
+	 */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -346,6 +423,73 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and scan key where indicated by skipatts */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* won't skip using this attribute */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[output_ikey];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * Temporary testing GUC can disable the use of skip support
+			 * routines
+			 */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how the
+			 * consed-up "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[output_ikey], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			output_ikey++;		/* keep this scan key/array */
+		}
+
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
@@ -520,6 +664,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
 		numArrayKeys++;
 		output_ikey++;			/* keep this scan key/array */
 	}
@@ -634,7 +782,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -685,7 +834,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -695,6 +844,191 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_decide_skipatts() -- set index attributes requiring skip arrays
+ *
+ * _bt_preprocess_array_keys helper function.  Determines which attributes
+ * will require skip arrays/scan keys.  Also sets up skip support callbacks
+ * for attributes whose input opclass have skip support (opclasses without
+ * skip support will fall back on using next-key sentinel values when
+ * advancing the skip array to its next array element).
+ *
+ * Return value is the total number of scan keys to add as "input" scan keys
+ * for further processing within _bt_preprocess_keys.
+ */
+static int
+_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_inkey = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+		int			prev_numSkipArrayKeys = numSkipArrayKeys;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				return prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			numSkipArrayKeys++;
+			attno_skip++;
+		}
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i >= scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 * (either in part or in whole)
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				numSkipArrayKeys++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				return numSkipArrayKeys;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	return numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatts
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatts)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatts->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										  BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack an equality operator, but
+	 * they're supported.  Cope with them by having caller not use skip scan.
+	 */
+	if (!OidIsValid(skipatts->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatts->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+														reverse,
+														&skipatts->sksup);
+
+	/* might not have set up skip support routine, but can skip either way */
+	return true;
+}
+
 /*
  * _bt_setup_array_cmp() -- Set up array comparison functions
  *
@@ -987,17 +1321,15 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
@@ -1010,8 +1342,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.  We have to be sure
+	 * that _bt_compare_array_skey/_bt_binsrch_array_skey use the right proc.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1042,11 +1374,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensured that the scalar scan key could be eliminated as redundant
+		 */
+		eliminated = true;
+	}
+	else
+	{
+		/*
+		 * With a skip array it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when earlier preprocessing wasn't able to eliminate a
+		 * redundant scan key inequality due to a lack of cross-type support.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when it
+ * is redundant with (or contradicted by) a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1098,6 +1484,137 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of skip array scan key when it is "redundant with"
+ * a non-array scalar scan key.  The scalar scan key must be an inequality.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array, allowing its elements to be generated within the limits of a range.
+ * Calling here always renders caller's scalar scan key redundant (the key is
+ * applied when the array advances, but that's just an implementation detail).
+ *
+ * Return value indicates if the array already had a lower/upper bound
+ * (whichever caller's scalar scan key was expected to be).  We return true in
+ * the common case where caller's scan key could be successfully rolled into
+ * the skip array.  We return false when we can't do that due to the presence
+ * of a conflicting inequality.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	/*
+	 * We don't expect to have to deal with NULLs in non-array/non-skip scan
+	 * key.  We expect _bt_preprocess_array_keys to avoid generating a skip
+	 * array for an index attribute with an IS NULL input scan key.  (It will
+	 * still do so in the presence of IS NOT NULL input scan keys, but
+	 * _bt_compare_scankey_args is expected to handle those for us.)
+	 */
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(arraysk->sk_flags & SK_SEARCHARRAY);
+	Assert(arraysk->sk_strategy == BTEqualStrategyNumber);
+	Assert(array->num_elems == -1);
+
+	/* Scalar scan key must be a B-Tree inequality, which are always strict */
+	Assert(!(skey->sk_flags & SK_ISNULL));
+	Assert(skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * A skip array scan key always uses the underlying index attribute's
+	 * input opclass, but it's possible that caller's scalar scan key uses a
+	 * cross-type operator.  In cross-type scenarios, skey.sk_argument doesn't
+	 * use the same type as later array elements (which are all just copies of
+	 * datums taken from index tuples, possibly modified by skip support).
+	 *
+	 * We represent the lowest (and highest) possible value in the array using
+	 * the sentinel value -inf (+inf for high_compare).  The only exceptions
+	 * apply when the opclass has skip support: there we can use a copy of the
+	 * skip support routine's low_elem/high_elem instead -- though only when
+	 * there is no corresponding low_compare/high_compare inequality.
+	 *
+	 * _bt_first understands that -inf/+inf indicate that it should use the
+	 * low_compare/high_compare inequality for initial positioning purposes
+	 * when it sees either value (unless there is no corresponding inequality,
+	 * in which case the values are literally interpreted as -inf or +inf).
+	 * _bt_first can therefore vary in whether it uses a cross-type operator,
+	 * or an input-opclass-only operator (it can vary across primitive scans
+	 * for the same index attribute/skip array).
+	 *
+	 * _bt_scankey_decrement/_bt_scankey_increment both make sure that each
+	 * newly generated element is constrained by low_compare/high_compare.
+	 * This must happen without skey.sk_argument ever being treated as a true
+	 * array element (that wouldn't always work because array elements are
+	 * only ever supposed to use the opclass input type).
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare */
+
+				/* replace old high_compare with new one */
+			}
+			else
+				array->high_compare = palloc(sizeof(ScanKeyData));
+
+			memcpy(array->high_compare, skey, sizeof(ScanKeyData));
+			array->order_high = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare */
+
+				/* replace old low_compare with new one */
+			}
+			else
+				array->low_compare = palloc(sizeof(ScanKeyData));
+
+			memcpy(array->low_compare, skey, sizeof(ScanKeyData));
+			array->order_low = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
@@ -1140,7 +1657,8 @@ _bt_compare_array_elements(const void *a, const void *b, void *arg)
 static inline int32
 _bt_compare_array_skey(FmgrInfo *orderproc,
 					   Datum tupdatum, bool tupnull,
-					   Datum arrdatum, ScanKey cur)
+					   Datum arrdatum, bool arrnull,
+					   ScanKey cur)
 {
 	int32		result = 0;
 
@@ -1148,14 +1666,14 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 
 	if (tupnull)				/* NULL tupdatum */
 	{
-		if (cur->sk_flags & SK_ISNULL)
+		if (arrnull)
 			result = 0;			/* NULL "=" NULL */
 		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = -1;		/* NULL "<" NOT_NULL */
 		else
 			result = 1;			/* NULL ">" NOT_NULL */
 	}
-	else if (cur->sk_flags & SK_ISNULL) /* NOT_NULL tupdatum, NULL arrdatum */
+	else if (arrnull)			/* NOT_NULL tupdatum, NULL arrdatum */
 	{
 		if (cur->sk_flags & SK_BT_NULLS_FIRST)
 			result = 1;			/* NOT_NULL ">" NULL */
@@ -1221,6 +1739,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1256,7 +1776,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[low_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result <= 0)
 				{
@@ -1284,7 +1804,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 			{
 				arrdatum = array->elem_values[high_elem];
 				result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-												arrdatum, cur);
+												arrdatum, false, cur);
 
 				if (result >= 0)
 				{
@@ -1311,7 +1831,7 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 		arrdatum = array->elem_values[mid_elem];
 
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										arrdatum, cur);
+										arrdatum, false, cur);
 
 		if (result == 0)
 		{
@@ -1336,13 +1856,102 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	 */
 	if (low_elem != mid_elem)
 		result = _bt_compare_array_skey(orderproc, tupdatum, tupnull,
-										array->elem_values[low_elem], cur);
+										array->elem_values[low_elem], false,
+										cur);
 
 	*set_elem_result = result;
 
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * This routine doesn't return an index into the array, because the array
+ * doesn't actually have any elements (it generates its array elements
+ * procedurally instead).  Note that this may include a NULL value/an IS NULL
+ * qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1352,29 +1961,486 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
 		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
 		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, curArrayKey,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppoDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Clear possibly-irrelevant flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Lowest (or highest) element is NULL, so set scan key to NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Lowest array element isn't NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Highest array element isn't NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		underflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & SK_BT_NEXTPRIOR));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the NEXTPRIOR flag.  The true prior value can only
+	 * be determined when the scan reads lower sorting tuples.
+	 *
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, _bt_first can find the highest non-NULL.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * low_compare is for an >= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true prior value
+		 * cannot possibly satisfy low_compare.  We can give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(&array->order_low,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, false,
+								   skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true prior value */
+		skey->sk_flags |= SK_BT_NEXTPRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &underflow);
+
+	if (underflow)
+	{
+		/* dec_sk_argument has undefined value due to underflow */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the skip array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		overflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & SK_BT_NEXTPRIOR));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXTPRIOR flag.  The true next value can only be
+	 * determined when the scan reads higher sorting tuples.
+	 *
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, _bt_first can find the lowest non-NULL.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * high_compare is for an <= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true next value cannot
+		 * possibly satisfy high_compare.  We can give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(&array->order_high,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, false,
+								   skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true next value */
+		skey->sk_flags |= SK_BT_NEXTPRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &overflow);
+
+	if (overflow)
+	{
+		/* inc_sk_argument has undefined value due to overflow */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the skip array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1390,6 +2456,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1399,29 +2466,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1476,6 +2544,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1483,7 +2552,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1495,16 +2563,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1629,9 +2691,70 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_NEGPOSINF))
+		{
+			/* The scankey has a conventional sk_argument/element value */
+			Datum		sk_argument = cur->sk_argument;
+			bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
+			bool		sk_nextprior = (cur->sk_flags & SK_BT_NEXTPRIOR) != 0;
+
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											sk_argument, sk_isnull, cur);
+
+			/*
+			 * When scan key is marked NEXTPRIOR, the current array element is
+			 * "sk_argument + infinitesimal" (or the current array element is
+			 * "sk_argument - infinitesimal", during backwards scans)
+			 */
+			if (result == 0 && sk_nextprior)
+			{
+				/*
+				 * tupdatum is actually still < "sk_argument + infinitesimal"
+				 * (or it's actually still > "sk_argument - infinitesimal")
+				 */
+				return true;
+			}
+		}
+		else
+		{
+			/*
+			 * The scankey searches for the sentinel value -inf/+inf.
+			 *
+			 * Note: -inf could mean "absolute" -inf, or it could represent
+			 * the highest possible value that still doesn't satisfy the
+			 * array's low_compare.  +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * Compare tupdatum against -inf using array's low_compare, if any
+			 * (or compare it against +inf using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is > -inf sk_argument (or < +inf sk_argument).
+				 * It's time for caller to advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1963,18 +3086,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1999,18 +3113,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2028,15 +3133,27 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
+			Datum		sk_argument = cur->sk_argument;
+			bool		sk_isnull = (cur->sk_flags & SK_ISNULL) != 0;
+
 			Assert(sktrig_required && required);
 
 			/*
@@ -2050,7 +3167,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 */
 			result = _bt_compare_array_skey(&so->orderProcs[ikey],
 											tupdatum, tupnull,
-											cur->sk_argument, cur);
+											sk_argument, sk_isnull, cur);
 		}
 
 		/*
@@ -2109,11 +3226,65 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  This is often the range -inf through to +inf.
+		 * "Binary searching" a skip array only determines whether tupdatum is
+		 * beyond its range, before its range, or within its range.
+		 *
+		 * Note: conventional arrays cannot use this approach.  They need
+		 * "beyond end of array element" advancement to distinguish between
+		 * the final array element (where incremental advancement rolls over
+		 * to the next most significant array), and some earlier array element
+		 * (where incremental advancement just increments set_elem/cur_elem).
+		 * That distinction doesn't exist when dealing with range skip arrays.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == some particular skip array element.
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2463,6 +3634,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also cons up skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2579,6 +3752,19 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+		{
+			if (so->keyData)
+				so->keyData = repalloc(so->keyData,
+									   numberOfKeys * sizeof(ScanKeyData));
+			else
+				so->keyData = palloc(numberOfKeys * sizeof(ScanKeyData));
+		}
 	}
 	else
 		inkeys = scan->keyData;
@@ -2718,7 +3904,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
+						Assert(!array || array->num_elems > 0 ||
+							   array->num_elems == -1);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -2881,7 +4068,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
+					Assert(!array || array->num_elems > 0 ||
+						   array->num_elems == -1);
 
 					/*
 					 * New key is more restrictive, and so replaces old key...
@@ -3023,10 +4211,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3101,6 +4290,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3174,6 +4379,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3737,6 +4943,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXTPRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index db6ed784a..86a5de8f4 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 8efb4044d..6efa3e353 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -372,6 +372,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index edb09d4e3..e945686c8 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -96,6 +96,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 8130f3e8a..256350007 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index 8c6fc80c3..91682edd5 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -83,6 +83,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 03d7fb5f4..78864b15d 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -192,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -213,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5733,6 +5737,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6791,6 +6881,54 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a
+ * single-column index, or C * 0.75 for multiple columns. (The idea here
+ * is that multiple columns dilute the importance of the first column's
+ * ordering, but don't negate it entirely.  Before 8.0 we divided the
+ * correlation by the number of columns, but that seems too strong.)
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6800,17 +6938,21 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6826,13 +6968,17 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
@@ -6843,13 +6989,81 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6891,7 +7105,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6907,6 +7121,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6922,6 +7168,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -7030,104 +7277,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 5284d23dc..654bda638 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -384,6 +387,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 686309db5..faa3a678f 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1751,6 +1752,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3590,6 +3602,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..9662fb2ba 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -583,6 +583,19 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure via an index <quote>skip scan</quote>.  The
+     APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..433e108b8 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intevening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,10 +511,7 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
+   Multicolumn indexes should be used judiciously.  See
    <xref linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
@@ -669,9 +669,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..8b6b775c1 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index cf6eac573..f7b3ecef4 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1938,7 +1938,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -1958,7 +1958,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 31fb7d142..8c2a939b0 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4370,24 +4370,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -7482,19 +7483,23 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                        QUERY PLAN                         
------------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Nested Loop
    ->  Hash Join
          Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-         ->  Seq Scan on fkest f2
-               Filter: (x100 = 2)
+         ->  Bitmap Heap Scan on fkest f2
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
          ->  Hash
-               ->  Seq Scan on fkest f1
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f1
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
          Index Cond: (x = f1.x)
-(10 rows)
+(14 rows)
 
 alter table fkest add constraint fk
   foreign key (x, x10b, x100) references fkest (x, x10, x100);
@@ -7503,20 +7508,24 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                     QUERY PLAN                      
------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Hash Join
    Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
    ->  Hash Join
          Hash Cond: (f3.x = f2.x)
          ->  Seq Scan on fkest f3
          ->  Hash
-               ->  Seq Scan on fkest f2
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f2
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Hash
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-(11 rows)
+         ->  Bitmap Heap Scan on fkest f1
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
+(15 rows)
 
 rollback;
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 3819bf5e2..58c2d013a 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5240,9 +5240,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 0456d48c9..39aa1f89e 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1461,18 +1461,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..4246afefd 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index e296891ca..1d269dc30 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -766,7 +766,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -776,7 +776,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b6135f034..d01625873 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2662,6 +2663,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

v8-0002-Normalize-nbtree-truncated-high-key-array-behavio.patchapplication/octet-stream; name=v8-0002-Normalize-nbtree-truncated-high-key-array-behavio.patchDownload
From 042f389da786aa4ff8538292367d8d9c6ff9b2ba Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Thu, 8 Aug 2024 13:51:18 -0400
Subject: [PATCH v8 2/3] Normalize nbtree truncated high key array behavior.

Commit 5bf748b8 taught nbtree ScalarArrayOp array processing to decide
when and how to start the next primitive index scan based on physical
index characteristics.  This included rules for deciding whether to
start a new primitive index scan (or whether to move onto the right
sibling leaf page instead) whenever the scan encounters a leaf high key
with truncated lower-order columns whose omitted/-inf values are covered
by one or more arrays.

Prior to this commit, nbtree would treat a truncated column as
satisfying a scan key that marked required in the current scan
direction.  It would just give up and start a new primitive index scan
in cases involving inequalities required in the opposite direction only
(in practice this meant > and >= strategy scan keys, since only forward
scans consider the page high key like this).

Bring > and >= strategy scan keys in line with other required scan key
types: have nbtree persist with its current primitive index scan
regardless of the operator strategy in use.  This requires scheduling
and then performing an explicit check of the next page's high key (if
any) at the point that _bt_readpage is next called.

Although this could be considered a stand alone piece of work, it's
mostly intended as preparation for an upcoming patch that adds skip scan
optimizations to nbtree.  Without this work there are cases where the
scan's skip arrays trigger an excessive number of primitive index scans
due to most high keys having a truncated attribute that was previously
treated as not satisfying a required > or >= strategy scan key.

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=9A_UtM7HzUThSkQ+BcrQsQZuNhWOvQWK06PRkEp=SKQ@mail.gmail.com
---
 src/include/access/nbtree.h           |   3 +
 src/backend/access/nbtree/nbtree.c    |   4 +
 src/backend/access/nbtree/nbtsearch.c |  22 +++++
 src/backend/access/nbtree/nbtutils.c  | 119 ++++++++++++++------------
 4 files changed, 95 insertions(+), 53 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index d64300fb9..d709fe08d 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1048,6 +1048,7 @@ typedef struct BTScanOpaqueData
 	int			numArrayKeys;	/* number of equality-type array keys */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Last array advancement matched -inf attr? */
+	bool		oppoDirCheck;	/* check opposite dir scan keys? */
 	BTArrayKeyInfo *arrayKeys;	/* info about each equality-type array key */
 	FmgrInfo   *orderProcs;		/* ORDER procs for required equality keys */
 	MemoryContext arrayContext; /* scan-lifespan context for array data */
@@ -1289,6 +1290,8 @@ extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir);
 extern void _bt_preprocess_keys(IndexScanDesc scan);
 extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 						  IndexTuple tuple, int tupnatts);
+extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+								  IndexTuple finaltup);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index b413433d9..f166c7549 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -333,6 +333,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 
 	so->needPrimScan = false;
 	so->scanBehind = false;
+	so->oppoDirCheck = false;
 	so->arrayKeys = NULL;
 	so->orderProcs = NULL;
 	so->arrayContext = NULL;
@@ -376,6 +377,7 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 	so->markItemIndex = -1;
 	so->needPrimScan = false;
 	so->scanBehind = false;
+	so->oppoDirCheck = false;
 	BTScanPosUnpinIfPinned(so->markPos);
 	BTScanPosInvalidate(so->markPos);
 
@@ -621,6 +623,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 		 */
 		so->needPrimScan = false;
 		so->scanBehind = false;
+		so->oppoDirCheck = false;
 	}
 	else
 	{
@@ -679,6 +682,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			 */
 			so->needPrimScan = true;
 			so->scanBehind = false;
+			so->oppoDirCheck = false;
 		}
 		else if (btscan->btps_pageStatus != BTPARALLEL_ADVANCING)
 		{
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index b11112539..fbf474377 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1704,6 +1704,28 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			ItemId		iid = PageGetItemId(page, P_HIKEY);
 
 			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+			if (unlikely(so->oppoDirCheck))
+			{
+				/*
+				 * Last _bt_readpage call scheduled precheck of finaltup for
+				 * required scan keys up to and including a > or >= scan key
+				 * (necessary because > and >= are only generally considered
+				 * required when scanning backwards)
+				 */
+				Assert(so->scanBehind);
+				so->oppoDirCheck = false;
+				if (!_bt_oppodir_checkkeys(scan, dir, pstate.finaltup))
+				{
+					/*
+					 * Back out of continuing with this leaf page -- schedule
+					 * another primitive index scan after all
+					 */
+					so->currPos.moreRight = false;
+					so->needPrimScan = true;
+					return false;
+				}
+			}
 		}
 
 		/* load items[] in ascending order */
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index b9f814969..025e20967 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -1372,7 +1372,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 			curArrayKey->cur_elem = 0;
 		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
 	}
-	so->scanBehind = false;
+	so->scanBehind = so->oppoDirCheck = false;	/* reset */
 }
 
 /*
@@ -1681,8 +1681,7 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
 
 	Assert(so->numArrayKeys);
 
-	/* scanBehind flag doesn't persist across primitive index scans - reset */
-	so->scanBehind = false;
+	so->scanBehind = so->oppoDirCheck = false;	/* reset */
 
 	/*
 	 * Array keys are advanced within _bt_checkkeys when the scan reaches the
@@ -1818,7 +1817,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 
-		so->scanBehind = false; /* reset */
+		so->scanBehind = so->oppoDirCheck = false;	/* reset */
 
 		/*
 		 * Required scan key wasn't satisfied, so required arrays will have to
@@ -2303,19 +2302,27 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	if (so->scanBehind && has_required_opposite_direction_only)
 	{
 		/*
-		 * However, we avoid this behavior whenever the scan involves a scan
+		 * However, we do things differently whenever the scan involves a scan
 		 * key required in the opposite direction to the scan only, along with
 		 * a finaltup with at least one truncated attribute that's associated
 		 * with a scan key marked required (required in either direction).
 		 *
 		 * _bt_check_compare simply won't stop the scan for a scan key that's
 		 * marked required in the opposite scan direction only.  That leaves
-		 * us without any reliable way of reconsidering any opposite-direction
+		 * us without an automatic way of reconsidering any opposite-direction
 		 * inequalities if it turns out that starting a new primitive index
 		 * scan will allow _bt_first to skip ahead by a great many leaf pages
 		 * (see next section for details of how that works).
+		 *
+		 * We deal with this by explicitly scheduling a finaltup recheck for
+		 * the next page -- we'll call _bt_oppodir_checkkeys for the next
+		 * page's finaltup instead.  You can think of this as a way of dealing
+		 * with this page's finaltup being truncated by checking the next
+		 * page's finaltup instead.  And you can think of the oppoDirCheck
+		 * recheck handling within _bt_readpage as complementing the similar
+		 * scanBehind recheck made from within _bt_checkkeys.
 		 */
-		goto new_prim_scan;
+		so->oppoDirCheck = true;	/* schedule next page's finaltup recheck */
 	}
 
 	/*
@@ -2353,54 +2360,16 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * also before the _bt_first-wise start of tuples for our new qual.  That
 	 * at least suggests many more skippable pages beyond the current page.
 	 */
-	if (has_required_opposite_direction_only && pstate->finaltup &&
-		(all_required_satisfied || oppodir_inequality_sktrig))
+	else if (has_required_opposite_direction_only && pstate->finaltup &&
+			 (all_required_satisfied || oppodir_inequality_sktrig) &&
+			 unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup)))
 	{
-		int			nfinaltupatts = BTreeTupleGetNAtts(pstate->finaltup, rel);
-		ScanDirection flipped;
-		bool		continuescanflip;
-		int			opsktrig;
-
 		/*
-		 * We're checking finaltup (which is usually not caller's tuple), so
-		 * cannot reuse work from caller's earlier _bt_check_compare call.
-		 *
-		 * Flip the scan direction when calling _bt_check_compare this time,
-		 * so that it will set continuescanflip=false when it encounters an
-		 * inequality required in the opposite scan direction.
+		 * Make sure that any non-required arrays are set to the first array
+		 * element for the current scan direction
 		 */
-		Assert(!so->scanBehind);
-		opsktrig = 0;
-		flipped = -dir;
-		_bt_check_compare(scan, flipped,
-						  pstate->finaltup, nfinaltupatts, tupdesc,
-						  false, false, false,
-						  &continuescanflip, &opsktrig);
-
-		/*
-		 * Only start a new primitive index scan when finaltup has a required
-		 * unsatisfied inequality (unsatisfied in the opposite direction)
-		 */
-		Assert(all_required_satisfied != oppodir_inequality_sktrig);
-		if (unlikely(!continuescanflip &&
-					 so->keyData[opsktrig].sk_strategy != BTEqualStrategyNumber))
-		{
-			/*
-			 * It's possible for the same inequality to be unsatisfied by both
-			 * caller's tuple (in scan's direction) and finaltup (in the
-			 * opposite direction) due to _bt_check_compare's behavior with
-			 * NULLs
-			 */
-			Assert(opsktrig >= sktrig); /* not opsktrig > sktrig due to NULLs */
-
-			/*
-			 * Make sure that any non-required arrays are set to the first
-			 * array element for the current scan direction
-			 */
-			_bt_rewind_nonrequired_arrays(scan, dir);
-
-			goto new_prim_scan;
-		}
+		_bt_rewind_nonrequired_arrays(scan, dir);
+		goto new_prim_scan;
 	}
 
 	/*
@@ -3512,7 +3481,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 *
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
-		Assert(!so->scanBehind && !pstate->prechecked && !pstate->firstmatch);
+		Assert(!so->scanBehind && !so->oppoDirCheck);
+		Assert(!pstate->prechecked && !pstate->firstmatch);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
@@ -3624,6 +3594,49 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 								  ikey, true);
 }
 
+/*
+ * Test whether an indextuple satisfies inequalities required in the opposite
+ * direction only (and lower-order equalities required in either direction).
+ *
+ * scan: index scan descriptor (containing a search-type scankey)
+ * dir: current scan direction (flipped by us to get opposite direction)
+ * finaltup: final index tuple on the page
+ *
+ * Caller's finaltup tuple is the page high key (for forwards scans), or the
+ * first non-pivot tuple (for backwards scans).  Caller during scans with
+ * required array keys.
+ *
+ * Return true if finatup satisfies keys, false if not.  If the tuple fails to
+ * pass the qual, then caller is should start another primitive index scan;
+ * _bt_first can efficiently relocate the scan to a far later leaf page.
+ *
+ * Note: we focus on required-in-opposite-direction scan keys (e.g. for a
+ * required > or >= key, assuming a forwards scan) because _bt_checkkeys() can
+ * always deal with required-in-current-direction scan keys on its own.
+ */
+bool
+_bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+					  IndexTuple finaltup)
+{
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	int			nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+	bool		continuescan;
+	ScanDirection flipped = -dir;
+	int			ikey = 0;
+
+	Assert(so->numArrayKeys);
+
+	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
+					  false, false, false, &continuescan, &ikey);
+
+	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
+		return false;
+
+	return true;
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
-- 
2.45.2

In reply to: Peter Geoghegan (#37)
3 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Sat, Sep 21, 2024 at 1:44 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v8. I still haven't worked through any of your feedback,
Tomas. Again, this revision is just to keep CFBot happy by fixing the
bit rot on the master branch created by my recent commits.

Attached is v9.

I think that v9-0002-Normalize-nbtree-truncated-high-key-array-behavio.patch
is close to committable. It's basically independent work, which would
be nice to get out of the way soon.

Highlights for v9:

* Fixed a bug affecting scans that use scrollable cursors: v9 splits
the previous SK_BT_NEXTPRIOR sentinel scan key flag into separate
SK_BT_NEXT and SK_BT_PRIOR flags, so it's no longer possible to
confuse "foo"+infinitesimal with "foo"-infinitesimal when the scan's
direction changes at exactly the wrong time.

* Worked through all of Tomas' feedback.

In more detail:

- v9-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patch has been
taught to divide the total number of index searches by nloop as
required (e.g., for nested loop joins), per Tomas. This doesn't make
much difference, either way, so if that's what people want I'm happy
to oblige.

- Separately, the same EXPLAIN ANALYZE patch now shows "Index
Searches: 0" in cases where the scan node is never executed. (This was
already possible in cases where the scan node was executed, only for
_bt_preprocess_keys to determine that the scan's qual was
contradictory.)

- Various small updates to comments/symbol names, again based on
feedback from Tomas.

- Various small updates to the user sgml docs, again based on feedback
from Tomas.

- I've polished the commit messages for all 3 patches, particularly
the big one (v9-0003-Add-skip-scan-to-nbtree.patch).

- I haven't done anything about fixing any of the known regressions in
v9. I'm not aware that Tomas expects me to fix any regressions
highlighted by his recent testing (only regressions that I've been
aware of before Tomas became involved). Tomas should correct me if I
have that wrong, though.

Obviously, the #1 open item right now remains fixing the known
regressions in cases where skip scan should be attempted, but cannot
possibly help.

Thanks
--
Peter Geoghegan

Attachments:

v9-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v9-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From 2e726a01a75c793149eda82c830b27b37568dc80 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v9 1/3] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 doc/src/sgml/bloom.sgml                       |   6 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 21 files changed, 242 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 114a85dc4..361c33fca 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -131,6 +131,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 60853a0f6..879d5589d 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -582,6 +582,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index f2fd62afb..5e423e155 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index b35b8a975..36f1435cb 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index 0d99d6abc..927ba1039 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 4ec43e3c0..c589e5e8c 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -116,6 +116,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 56e502c4f..4febe6bdc 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -70,6 +70,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -550,6 +551,7 @@ btinitparallelscan(void *target)
 	SpinLockInit(&bt_target->btps_mutex);
 	bt_target->btps_scanPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -575,6 +577,7 @@ btparallelrescan(IndexScanDesc scan)
 	SpinLockAcquire(&btscan->btps_mutex);
 	btscan->btps_scanPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -683,6 +686,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			*pageno = btscan->btps_scanPage;
 			exit_loop = true;
@@ -764,6 +772,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -797,6 +807,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 	{
 		btscan->btps_scanPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index fff7c89ea..b11112539 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -963,6 +963,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 301786185..be668abf2 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index ee1bcb84e..ee68fab44 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -89,6 +90,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -1995,6 +1997,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2008,6 +2011,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2024,6 +2028,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2542,6 +2547,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 19f2b172c..92b13f539 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -170,9 +170,10 @@ CREATE INDEX
    Heap Blocks: exact=28
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..1792.00 rows=2 width=0) (actual time=0.356..0.356 rows=29 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 0.408 ms
-(8 rows)
+(9 rows)
 </programlisting>
   </para>
 
@@ -202,11 +203,12 @@ CREATE INDEX
    -&gt;  BitmapAnd  (cost=24.34..24.34 rows=2 width=0) (actual time=0.027..0.027 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..12.04 rows=500 width=0) (actual time=0.026..0.026 rows=0 loops=1)
                Index Cond: (i5 = 123451)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
-(9 rows)
+(10 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index db7f35a45..4bb1432b9 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4153,12 +4153,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index ff689b652..1f2172960 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -702,8 +702,10 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Heap Blocks: exact=10
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 1
  Planning Time: 0.485 ms
  Execution Time: 0.073 ms
 </screen>
@@ -754,6 +756,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Heap Blocks: exact=90
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
  Planning Time: 0.187 ms
  Execution Time: 3.036 ms
 </screen>
@@ -819,6 +822,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -848,9 +852,11 @@ EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique
          Buffers: shared hit=4 read=3
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared hit=2
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
                Buffers: shared hit=2 read=3
  Planning:
    Buffers: shared hit=3
@@ -883,6 +889,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Heap Blocks: exact=90
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1017,6 +1024,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
  Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
  Planning Time: 0.077 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a854..e042638b7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -502,9 +502,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    Batches: 1  Memory Usage: 24kB
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(7 rows)
+(8 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index ae9ce9d8e..c24d56007 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 9ee09fe2f..1448179fb 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -312,7 +325,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(10 rows)
+               Index Searches: N
+(11 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -329,7 +343,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(10 rows)
+               Index Searches: N
+(11 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -349,6 +364,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -356,9 +372,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -366,8 +384,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -379,6 +398,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -387,11 +407,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7a03b4e36..e3e99272a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2657,47 +2661,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off)
@@ -2713,16 +2726,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2741,7 +2757,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off)
@@ -2757,16 +2773,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2787,7 +2806,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2877,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2897,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,17 +2986,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2982,17 +3013,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3064,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3048,17 +3091,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3112,17 +3161,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3144,17 +3199,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3482,12 +3543,14 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(15);
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3566,10 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(25);
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3617,17 @@ explain (analyze, costs off, summary off, timing off) select * from ma_test wher
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4197,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 33a6dceb0..b7cf35b9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index 2eaeb1477..9afe205e0 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 442428d93..085e746af 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

v9-0003-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v9-0003-Add-skip-scan-to-nbtree.patchDownload
From e5f3324fba70e63add7b1f854b7313b9d41030bc Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v9 3/3] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
useful in cases where the total number of distinct values in the column
'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.

The design of skip arrays allows array advancement to work without
concern for which specific array types are involved; only the lowest
level code needs to treat skip arrays and SAOP arrays differently.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values are conceptually just like any
other value that might appear in either kind of array.  A call made to
_bt_first to reposition the scan based on a NEXT/PRIOR sentinel usually
won't locate a leaf page containing tuples that must be returned to the
scan, but that's normal during any skip scan that happens to involve
"sparse" indexed values (skip support can only help with "dense" indexed
values, which isn't meaningfully possible when the skipped column is of
a continuous type, where skip support typically can't be implemented).

The optimizer doesn't use distinct new index paths to represent index
skip scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  Skipping is possible during
eligible bitmap index scans, index scans, and index-only scans.  It's
also possible during eligible parallel scans.  An eligible scan is any
scan that nbtree preprocessing generates a skip array for, which happens
whenever doing so will enable later preprocessing to mark original input
scan keys (passed by the executor) as required to continue the scan.

There are hardly any limitations around where skip arrays/scan keys may
appear relative to conventional/input scan keys.  This is no less true
in the presence of conventional SAOP array scan keys, which may both
roll over and be rolled over by skip arrays.  For example, a skip array
on the column "b" is generated for "WHERE a = 42 AND c IN (5, 6, 7, 8)".
As always (since commit 5bf748b8), whether or not nbtree actually skips
depends in large part on physical index characteristics at runtime.
Preprocessing is optimistic about skipping working out: it applies
simple static rules to decide where to place skip arrays.  It's up to
array-related scan code to keep the overhead of maintaining the scan's
skip arrays under control when it turns out that skipping isn't helpful.

Preprocessing will never cons up a skip array for an index column that
has an equality strategy scan key on input, but will do so for indexed
columns that are only constrained by inequality strategy scan keys.
This allows the scan to skip using a range skip array.  These are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   28 +-
 src/include/catalog/pg_amproc.dat             |   16 +
 src/include/catalog/pg_proc.dat               |   24 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  109 ++
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  273 ++++
 src/backend/access/nbtree/nbtree.c            |  205 ++-
 src/backend/access/nbtree/nbtsearch.c         |  101 +-
 src/backend/access/nbtree/nbtutils.c          | 1448 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   46 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  375 +++--
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/uuid.c                  |   71 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/btree.sgml                       |   32 +
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   49 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |    6 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   61 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    2 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 34 files changed, 2706 insertions(+), 311 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c51de742e..0826c124f 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -202,7 +202,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 38b600945..381a4c59f 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1031,10 +1033,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard arrays that store elements in memory */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup set to valid routine? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* opclass skip scan support, when use_sksup */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo	order_low;		/* low_compare's ORDER proc */
+	FmgrInfo	order_high;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1124,6 +1138,10 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array, for skip scan */
+#define SK_BT_NEGPOSINF	0x00080000	/* no sk_argument, -inf/+inf key */
+#define SK_BT_NEXT		0x00100000	/* positions scan > sk_argument */
+#define SK_BT_PRIOR		0x00200000	/* positions scan < sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1160,6 +1178,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1170,7 +1192,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5d7fe292b..c0ccdfdfb 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -122,6 +128,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +149,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +170,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +205,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +275,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 43f608d7a..14ce6c79c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2250,6 +2265,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4437,6 +4455,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -9294,6 +9315,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d70e6d37e..5b58739ad 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..d91390fc6
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,109 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * This happens at the point where the scan determines that another primitive
+ * index scan is required.  The next value is used (in combination with at
+ * least one additional lower-order non-skip key, taken from the SQL query) to
+ * relocate the scan, skipping over many irrelevant leaf pages in the process.
+ *
+ * Skip support generally works best with discrete types such as integer,
+ * date, and boolean; types where there is a decent chance that indexes will
+ * contain contiguous values (given a leading attributes using the opclass).
+ * When gaps/discontinuities are naturally rare (e.g., a leading identity
+ * column in a composite index, a date column preceding a product_id column),
+ * then it makes sense for skip scans to optimistically assume that the next
+ * distinct indexable value will find directly matching index tuples.
+ *
+ * The B-Tree code can fall back on next-key sentinel values for any opclass
+ * that doesn't provide its own skip support function.  There is no point in
+ * providing skip support unless the next indexed key value is often the next
+ * indexable value (at least with some workloads).  Opclasses where that never
+ * works out in practice should just rely on the B-Tree AM's generic next-key
+ * fallback strategy.  Opclasses where adding skip support is infeasible or
+ * hard (e.g., an opclass for a continuous type) can also use the fallback.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called.
+ * If an opclass can only set some of the fields, then it cannot safely
+ * provide a skip support routine.
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming standard ascending
+	 * order).  This helps the B-Tree code with finding its initial position
+	 * at the leaf level (during the skip scan's first primitive index scan).
+	 * In other words, it gives the B-Tree code a useful value to start from,
+	 * before any data has been read from the index.
+	 *
+	 * low_elem and high_elem are also used by skip scans to determine when
+	 * they've reached the final possible value (in the current direction).
+	 * It's typical for the scan to run out of leaf pages before it runs out
+	 * of unscanned indexable values, but it's still useful for the scan to
+	 * have a way to recognize when it has reached the last possible value
+	 * (this saves us a useless probe that just lands on the final leaf page).
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (in the case of pass-by-reference
+	 * types).  It's not okay for these functions to leak any memory.
+	 *
+	 * Both decrement and increment callbacks are guaranteed to never be
+	 * called with a NULL "existing" arg.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is undefined, and the B-Tree
+	 * code is entitled to assume that no memory will have been allocated.
+	 *
+	 * The B-Tree skip scan caller's "existing" datum is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must be liberal
+	 * in accepting every possible representational variation within the
+	 * underlying data type.  On the other hand, opclasses are _not_ expected
+	 * to preserve any information that doesn't affect how datums are sorted
+	 * (e.g., skip support for a fixed precision numeric type isn't required
+	 * to preserve datum display scale).
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 1859be614..b4f1466d4 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..26ebbf776 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index ddc6e1f7a..f3aa83498 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -32,6 +32,7 @@
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
 #include "storage/smgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -71,7 +72,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -79,11 +80,21 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The reset of the space allocated in shared memory is also used when
+	 * scans need to schedule another primitive index scan.  It holds a
+	 * flattened representation of the backend's skip array datums, if any.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -536,10 +547,155 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
 	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Assume every index attribute might require that we generate a skip scan
+	 * key
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		Form_pg_attribute attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/* Restore skip array */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+
+		/* Now that old sk_argument memory is freed, copy over sk_flags */
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument to serialize */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -550,7 +706,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_scanPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	bt_target->btps_nsearches = 0;
@@ -572,15 +729,15 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_scanPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -607,6 +764,7 @@ btparallelrescan(IndexScanDesc scan)
 bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false;
 	bool		status = true;
@@ -640,7 +798,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -655,14 +813,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				*pageno = InvalidBlockNumber;
 				exit_loop = true;
 			}
@@ -699,7 +852,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			*pageno = btscan->btps_scanPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -729,10 +882,10 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber scan_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_scanPage = scan_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -769,7 +922,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -778,7 +931,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -796,6 +949,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -805,7 +959,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_scanPage == prev_scan_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -814,14 +968,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index d49e0fb70..be417faa0 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -883,7 +883,6 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	Buffer		buf;
 	BTStack		stack;
 	OffsetNumber offnum;
-	StrategyNumber strat;
 	BTScanInsertData inskey;
 	ScanKey		startKeys[INDEX_MAX_KEYS];
 	ScanKeyData notnullkeys[INDEX_MAX_KEYS];
@@ -975,7 +974,20 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * a > or < boundary or find an attribute with no boundary (which can be
 	 * thought of as the same as "> -infinity"), we can't use keys for any
 	 * attributes to its right, because it would break our simplistic notion
-	 * of what initial positioning strategy to use.
+	 * of what initial positioning strategy to use.  In practice skip scan
+	 * typically enables us to use all scan keys here, even with a set of
+	 * input keys that leave a "gap" between two index attributes (cases with
+	 * multiple gaps will even manage this without any special restrictions).
+	 *
+	 * Skip scan works by having _bt_preprocess_keys cons up = boundary keys
+	 * for any index columns that were missing a = key in scan->keyData[], the
+	 * input scan keys passed to us by the executor.  This happens for index
+	 * attributes prior to the attribute of our final input scan key.  The
+	 * underlying = keys use skip arrays.  The keys can be thought of as the
+	 * same as "col = ANY('{every possible col value}')".  Note that this
+	 * often includes the array element NULL, which the scan will treat as an
+	 * IS NULL qual (the skip array's scan key is already marked SK_SEARCHNULL
+	 * when we're called, so we need no special handling for this case here).
 	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
@@ -1050,6 +1062,45 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		{
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					/* -inf/+inf element from a skip array's scan key */
+					ScanKey		origchosen = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == chosen - so->keyData)
+							break;
+					}
+
+					/* use array's inequality key in startKeys[] */
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					Assert(!chosen ||
+						   chosen->sk_attno == origchosen->sk_attno);
+
+					/*
+					 * If the array does not include a NULL element (meaning
+					 * array advancement never generates an IS NULL qual),
+					 * we'll deduce a NOT NULL key to skip over any NULLs when
+					 * there's no usable low_compare (or no high_compare,
+					 * during a backwards scan).
+					 *
+					 * Note: this also handles an explicit NOT NULL key that
+					 * preprocessing folded into the skip array (the explicit
+					 * key will have been discarded as redundant with the array).
+					 */
+					if (!array->null_elem)
+						impliesNN = origchosen;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
 				/*
 				 * Done looking at keys for curattr.  If we didn't find a
 				 * usable boundary key, see if we can deduce a NOT NULL key.
@@ -1083,16 +1134,52 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 				startKeys[keysz++] = chosen;
 
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					/*
+					 * Next/prior key element from a skip array's scan key.
+					 * Adjust strat_total, so that our = key gets treated like
+					 * a > key (or like a < key) within _bt_search.
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).  This is only possible when the
+					 * index stores this column's NULLs at the same end of the
+					 * index that the scan starts at.  And only when the array
+					 * has a NULL element (so never with a range skip array).
+					 */
+					Assert(strat_total == BTEqualStrategyNumber);
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[]
+					 * (besides, doing so would confuse _bt_search, since it
+					 * isn't directly aware of NEXT or PRIOR sentinel values)
+					 */
+					break;
+				}
+
 				/*
 				 * Adjust strat_total, and quit if we have stored a > or <
 				 * key.
 				 */
-				strat = chosen->sk_strategy;
-				if (strat != BTEqualStrategyNumber)
+				if (chosen->sk_strategy != BTEqualStrategyNumber)
 				{
-					strat_total = strat;
-					if (strat == BTGreaterStrategyNumber ||
-						strat == BTLessStrategyNumber)
+					strat_total = chosen->sk_strategy;
+					if (chosen->sk_strategy == BTGreaterStrategyNumber ||
+						chosen->sk_strategy == BTLessStrategyNumber)
 						break;
 				}
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 75608034d..9abc35ebb 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the requiredness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using opclass skip
+ * support routines.  This can be used to quantify the peformance benefit that
+ * comes from having dedicated skip support, with a given test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup set to valid routine? */
+	Oid			eq_op;			/* InvalidOid means don't skip */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -64,15 +92,37 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno,
+							BTSkipPreproc *skipatts);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -256,6 +306,12 @@ _bt_freestack(BTStack stack)
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by later preprocessing on output.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -271,10 +327,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numArrayKeyData,
+				numSkipArrayKeys;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -282,11 +340,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
+	Assert(scan->numberOfKeys);
 
-	/* Quick check to see if there are any array keys */
+	/*
+	 * Quick check to see if there are any array keys, or any missing keys we
+	 * can generate a "skip scan" array key for ourselves
+	 */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	numArrayKeyData = scan->numberOfKeys;	/* Initial arrayKeyData[] size */
+	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
 		cur = &scan->keyData[i];
 		if (cur->sk_flags & SK_SEARCHARRAY)
@@ -302,6 +364,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		}
 	}
 
+	numSkipArrayKeys = _bt_decide_skipatts(scan, skipatts);
+	if (numSkipArrayKeys)
+	{
+		/* At least one skip array must be added to so->arrayKeys[] */
+		numArrayKeys += numSkipArrayKeys;
+		/* Associated scan keys must be added to arrayKeyData[], too */
+		numArrayKeyData += numSkipArrayKeys;
+	}
+
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
@@ -320,17 +391,23 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/*
+	 * Process each array key, and generate skip arrays as needed.  Also copy
+	 * every scan->keyData[] input scan key (whether it's an array or not)
+	 * into the arrayKeyData array we'll return to our caller (barring any
+	 * array scan keys that we could eliminate early through array merging).
+	 */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -346,16 +423,83 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and scan key where indicated by skipatts */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* won't skip using this attribute */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[numArrayKeyData];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * Temporary testing GUC can disable the use of skip support
+			 * routines
+			 */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how the
+			 * consed-up "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			numArrayKeyData++;	/* keep this scan key/array */
+		}
+
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
+		cur = &arrayKeyData[numArrayKeyData];
 		*cur = scan->keyData[input_ikey];
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
@@ -414,7 +558,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -425,7 +569,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -442,7 +586,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -517,17 +661,21 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -633,7 +781,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -684,7 +833,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -694,6 +843,192 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_decide_skipatts() -- set index attributes requiring skip arrays
+ *
+ * _bt_preprocess_array_keys helper function.  Determines which attributes
+ * will require skip arrays/scan keys.  Also sets up skip support callbacks
+ * for attributes whose input opclass have skip support (opclasses without
+ * skip support will fall back on using next-key sentinel values when
+ * advancing the skip array to its next array element).
+ *
+ * Return value is the total number of scan keys to add as "input" scan keys
+ * for further processing within _bt_preprocess_keys.
+ */
+static int
+_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_inkey = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+		int			prev_numSkipArrayKeys = numSkipArrayKeys;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				return prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			numSkipArrayKeys++;
+			attno_skip++;
+		}
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i >= scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 * (either in part or in whole)
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				numSkipArrayKeys++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				return numSkipArrayKeys;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	return numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatts
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatts)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatts->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										  BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack even an equality strategy
+	 * operator, but they're supported.  We cope by making caller not use a
+	 * skip array for this index column (nor for any lower-order columns).
+	 */
+	if (!OidIsValid(skipatts->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatts->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+														reverse,
+														&skipatts->sksup);
+
+	/* might not have set up skip support routine, but can skip either way */
+	return true;
+}
+
 /*
  * _bt_setup_array_cmp() -- Set up array comparison functions
  *
@@ -986,17 +1321,15 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
@@ -1009,8 +1342,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.  We have to be sure
+	 * that _bt_compare_array_skey/_bt_binsrch_array_skey use the right proc.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1041,11 +1374,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensured that the scalar scan key could be eliminated as redundant
+		 */
+		eliminated = true;
+	}
+	else
+	{
+		/*
+		 * With a skip array it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when earlier preprocessing wasn't able to eliminate a
+		 * redundant scan key inequality due to a lack of cross-type support.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when it
+ * is redundant with (or contradicted by) a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1097,6 +1484,138 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of skip array scan key when it is "redundant with"
+ * a non-array scalar scan key.  The scalar scan key must be an inequality.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array, allowing its elements to be generated within the limits of a range.
+ * Calling here always renders caller's scalar scan key redundant (the key is
+ * applied when the array advances, but that's just an implementation detail).
+ *
+ * Return value indicates if the array already had a lower/upper bound
+ * (whichever caller's scalar scan key was expected to be).  We return true in
+ * the common case where caller's scan key could be successfully rolled into
+ * the skip array.  We return false when we can't do that due to the presence
+ * of a conflicting inequality that cannot be compared to caller's inequality
+ * due to a lack of suitable cross-type support.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	/*
+	 * We don't expect to have to deal with NULLs in non-array/non-skip scan
+	 * key.  We expect _bt_preprocess_array_keys to avoid generating a skip
+	 * array for an index attribute with an IS NULL input scan key.  (It will
+	 * still do so in the presence of IS NOT NULL input scan keys, but
+	 * _bt_compare_scankey_args is expected to handle those for us.)
+	 */
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(arraysk->sk_flags & SK_SEARCHARRAY);
+	Assert(arraysk->sk_strategy == BTEqualStrategyNumber);
+	Assert(array->num_elems == -1);
+
+	/* Scalar scan key must be a B-Tree inequality, which are always strict */
+	Assert(!(skey->sk_flags & SK_ISNULL));
+	Assert(skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * A skip array scan key always uses the underlying index attribute's
+	 * input opclass, but it's possible that caller's scalar scan key uses a
+	 * cross-type operator.  In cross-type scenarios, skey.sk_argument doesn't
+	 * use the same type as later array elements (which are all just copies of
+	 * datums taken from index tuples, possibly modified by skip support).
+	 *
+	 * We represent the lowest (and highest) possible value in the array using
+	 * the sentinel value -inf (+inf for high_compare).  The only exceptions
+	 * apply when the opclass has skip support: there we can use a copy of the
+	 * skip support routine's low_elem/high_elem instead -- though only when
+	 * there is no corresponding low_compare/high_compare inequality.
+	 *
+	 * _bt_first understands that -inf/+inf indicate that it should use the
+	 * low_compare/high_compare inequality for initial positioning purposes
+	 * when it sees either value (unless there is no corresponding inequality,
+	 * in which case the values are literally interpreted as -inf or +inf).
+	 * _bt_first can therefore vary in whether it uses a cross-type operator,
+	 * or an input-opclass-only operator (it can vary across primitive scans
+	 * for the same index attribute/skip array).
+	 *
+	 * _bt_scankey_decrement/_bt_scankey_increment both make sure that each
+	 * newly generated element is constrained by low_compare/high_compare.
+	 * This must happen without skey.sk_argument ever being treated as a true
+	 * array element (that wouldn't always work because array elements are
+	 * only ever supposed to use the opclass input type).
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare */
+
+				/* replace old high_compare with new one */
+			}
+			else
+				array->high_compare = palloc(sizeof(ScanKeyData));
+
+			memcpy(array->high_compare, skey, sizeof(ScanKeyData));
+			array->order_high = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare */
+
+				/* replace old low_compare with new one */
+			}
+			else
+				array->low_compare = palloc(sizeof(ScanKeyData));
+
+			memcpy(array->low_compare, skey, sizeof(ScanKeyData));
+			array->order_low = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
@@ -1220,6 +1739,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1342,6 +1863,94 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * This routine doesn't return an index into the array, because the array
+ * doesn't actually have any elements (it generates its array elements
+ * procedurally instead).  Note that this may include a NULL value/an IS NULL
+ * qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1351,29 +1960,484 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
 		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
 		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, curArrayKey,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Clear possibly-irrelevant flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Lowest (or highest) element is NULL, so set scan key to NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Lowest array element isn't NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Highest array element isn't NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		underflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value can only be
+	 * determined when the scan reads lower sorting tuples.
+	 *
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, _bt_first can find the highest non-NULL.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * low_compare is for an >= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true prior value
+		 * cannot possibly satisfy low_compare.  We can give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(&array->order_low,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true prior value */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &underflow);
+
+	if (underflow)
+	{
+		/* dec_sk_argument has undefined value due to underflow */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the skip array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		overflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value can only be
+	 * determined when the scan reads higher sorting tuples.
+	 *
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, _bt_first can find the lowest non-NULL.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * high_compare is for an <= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true next value cannot
+		 * possibly satisfy high_compare.  We can give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(&array->order_high,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true next value */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &overflow);
+
+	if (overflow)
+	{
+		/* inc_sk_argument has undefined value due to overflow */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the skip array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1389,6 +2453,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1398,29 +2463,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1475,6 +2541,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1482,7 +2549,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1494,16 +2560,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1628,9 +2688,94 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_NEGPOSINF))
+		{
+			/* Scankey has a valid/comparable sk_argument value (not -inf) */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXT))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument + infinitesimal"
+				 */
+				if (ScanDirectionIsForward(dir))
+				{
+					/* tupdatum is still < "sk_argument + infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was forward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to backward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+			else if (result == 0 && (cur->sk_flags & SK_BT_PRIOR))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument - infinitesimal"
+				 */
+				if (ScanDirectionIsBackward(dir))
+				{
+					/* tupdatum is still > "sk_argument - infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was backward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to forward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+		}
+		else
+		{
+			/*
+			 * Scankey searches for the sentinel value -inf (or +inf).
+			 *
+			 * Note: -inf could mean "absolute" -inf, or it could represent an
+			 * imaginary point in the key space that comes immediately before
+			 * the first real value that satisfies the array's low_compare.
+			 * +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * Compare tupdatum against -inf using array's low_compare, if any
+			 * (or compare it against +inf using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is > -inf sk_argument (or < +inf sk_argument).
+				 * It's time for caller to advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1962,18 +3107,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1998,18 +3134,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2027,12 +3154,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -2108,11 +3244,65 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  This is often the range -inf through to +inf.
+		 * "Binary searching" a skip array only determines whether tupdatum is
+		 * beyond its range, before its range, or within its range.
+		 *
+		 * Note: conventional arrays cannot use this approach.  They need
+		 * "beyond end of array element" advancement to distinguish between
+		 * the final array element (where incremental advancement rolls over
+		 * to the next most significant array), and some earlier array element
+		 * (where incremental advancement just increments set_elem/cur_elem).
+		 * That distinction doesn't exist when dealing with range skip arrays.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == some particular skip array element.
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2462,6 +3652,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also cons up skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2578,6 +3770,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -2717,7 +3917,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
+						Assert(!array || array->num_elems > 0 ||
+							   array->num_elems == -1);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -2765,6 +3966,16 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * Our call to _bt_preprocess_array_keys is generally expected to
+			 * have already added "=" scan keys with skip arrays as required
+			 * to form an unbroken series of "=" constraints on all attrs
+			 * prior to the attr from the last scan key that came from our
+			 * original set of scan->keyData[] input scan keys.  Despite all
+			 * this, we cannot assume _bt_preprocess_array_keys will always
+			 * make it safe for us to mark all scan keys required.  Certain
+			 * corner cases might have prevented it from adding a skip array
+			 * (e.g., opclasses without an "=" operator can't use "=" arrays).
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -2880,7 +4091,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
+					Assert(!array || array->num_elems > 0 ||
+						   array->num_elems == -1);
 
 					/*
 					 * New key is more restrictive, and so replaces old key...
@@ -3022,10 +4234,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3100,6 +4313,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3173,6 +4402,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3736,6 +4966,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index db6ed784a..86a5de8f4 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 8efb4044d..6efa3e353 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -372,6 +372,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index edb09d4e3..e945686c8 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -96,6 +96,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 8130f3e8a..256350007 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index 8c6fc80c3..91682edd5 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -83,6 +83,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 03d7fb5f4..3bf552957 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -192,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -213,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5733,6 +5737,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6791,6 +6881,54 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a
+ * single-column index, or C * 0.75 for multiple columns. (The idea here
+ * is that multiple columns dilute the importance of the first column's
+ * ordering, but don't negate it entirely.  Before 8.0 we divided the
+ * correlation by the number of columns, but that seems too strong.)
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6800,17 +6938,21 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6826,13 +6968,17 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
@@ -6843,13 +6989,88 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6891,7 +7112,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6907,6 +7128,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6922,6 +7175,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -7030,104 +7284,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 5284d23dc..654bda638 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -384,6 +387,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 686309db5..faa3a678f 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1751,6 +1752,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3590,6 +3602,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..d256e091f 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..8b6b775c1 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index d3358dfc3..fa6f27e6d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1938,7 +1938,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -1958,7 +1958,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 31fb7d142..8c2a939b0 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4370,24 +4370,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -7482,19 +7483,23 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                        QUERY PLAN                         
------------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Nested Loop
    ->  Hash Join
          Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-         ->  Seq Scan on fkest f2
-               Filter: (x100 = 2)
+         ->  Bitmap Heap Scan on fkest f2
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
          ->  Hash
-               ->  Seq Scan on fkest f1
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f1
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
          Index Cond: (x = f1.x)
-(10 rows)
+(14 rows)
 
 alter table fkest add constraint fk
   foreign key (x, x10b, x100) references fkest (x, x10, x100);
@@ -7503,20 +7508,24 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                     QUERY PLAN                      
------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Hash Join
    Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
    ->  Hash Join
          Hash Cond: (f3.x = f2.x)
          ->  Seq Scan on fkest f3
          ->  Hash
-               ->  Seq Scan on fkest f2
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f2
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Hash
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-(11 rows)
+         ->  Bitmap Heap Scan on fkest f1
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
+(15 rows)
 
 rollback;
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 3819bf5e2..58c2d013a 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5240,9 +5240,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 0456d48c9..39aa1f89e 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1461,18 +1461,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..4246afefd 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index fe162cc7c..dea40e013 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -766,7 +766,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -776,7 +776,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b6135f034..d01625873 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2662,6 +2663,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

v9-0002-Normalize-nbtree-truncated-high-key-array-behavio.patchapplication/octet-stream; name=v9-0002-Normalize-nbtree-truncated-high-key-array-behavio.patchDownload
From e493af4514512a5d04b344d13aac52494facb87a Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Thu, 8 Aug 2024 13:51:18 -0400
Subject: [PATCH v9 2/3] Normalize nbtree truncated high key array behavior.

Commit 5bf748b8 taught nbtree ScalarArrayOp array processing to decide
when and how to start the next primitive index scan based on physical
index characteristics.  This included rules for deciding whether to
start a new primitive index scan (or whether to move onto the right
sibling leaf page instead) whenever the scan encounters a leaf high key
with truncated lower-order columns whose omitted/-inf values are covered
by one or more arrays.

Prior to this commit, nbtree only treated a truncated key column as
satisfying scan keys that were marked required in the scan's direction.
It would just give up and start a new primitive index scan in cases
involving inequalities marked required in the opposite direction only
(in practice this meant > and >= strategy scan keys, since only forward
scans consider the page high key like this).

Bring > and >= strategy scan keys in line with other required scan key
types: have nbtree persist with its current primitive index scan
regardless of the operator strategy in use.  This requires scheduling
and then performing an explicit check of the next page's high key (if
any) at the point that _bt_readpage is next called.

Although this could be considered a stand alone piece of work, it was
written in preparation for an upcoming patch to add skip scan to nbtree.
Without this enhancement, skip scan would create cases where the scan's
"skip arrays" trigger an excessive number of primitive index scans.  In
principle the underlying problem exists independently of whether skip
arrays or conventional SAOP arrays are used.  In practice skip arrays
tend to make array advancement a lot more sensitive to issues in this
area, so this work is more or less a prerequisite for skip scan.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Discussion: https://postgr.es/m/CAH2-Wz=9A_UtM7HzUThSkQ+BcrQsQZuNhWOvQWK06PRkEp=SKQ@mail.gmail.com
---
 src/include/access/nbtree.h           |   3 +
 src/backend/access/nbtree/nbtree.c    |   4 +
 src/backend/access/nbtree/nbtsearch.c |  22 +++++
 src/backend/access/nbtree/nbtutils.c  | 119 ++++++++++++++------------
 4 files changed, 95 insertions(+), 53 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index d64300fb9..38b600945 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1048,6 +1048,7 @@ typedef struct BTScanOpaqueData
 	int			numArrayKeys;	/* number of equality-type array keys */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Last array advancement matched -inf attr? */
+	bool		oppositeDirCheck;	/* check opposite dir scan keys? */
 	BTArrayKeyInfo *arrayKeys;	/* info about each equality-type array key */
 	FmgrInfo   *orderProcs;		/* ORDER procs for required equality keys */
 	MemoryContext arrayContext; /* scan-lifespan context for array data */
@@ -1289,6 +1290,8 @@ extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir);
 extern void _bt_preprocess_keys(IndexScanDesc scan);
 extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 						  IndexTuple tuple, int tupnatts);
+extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+								  IndexTuple finaltup);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 4febe6bdc..ddc6e1f7a 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -333,6 +333,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 
 	so->needPrimScan = false;
 	so->scanBehind = false;
+	so->oppositeDirCheck = false;
 	so->arrayKeys = NULL;
 	so->orderProcs = NULL;
 	so->arrayContext = NULL;
@@ -376,6 +377,7 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 	so->markItemIndex = -1;
 	so->needPrimScan = false;
 	so->scanBehind = false;
+	so->oppositeDirCheck = false;
 	BTScanPosUnpinIfPinned(so->markPos);
 	BTScanPosInvalidate(so->markPos);
 
@@ -621,6 +623,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 		 */
 		so->needPrimScan = false;
 		so->scanBehind = false;
+		so->oppositeDirCheck = false;
 	}
 	else
 	{
@@ -679,6 +682,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			 */
 			so->needPrimScan = true;
 			so->scanBehind = false;
+			so->oppositeDirCheck = false;
 		}
 		else if (btscan->btps_pageStatus != BTPARALLEL_ADVANCING)
 		{
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index b11112539..d49e0fb70 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1704,6 +1704,28 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			ItemId		iid = PageGetItemId(page, P_HIKEY);
 
 			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+			if (unlikely(so->oppositeDirCheck))
+			{
+				/*
+				 * Last _bt_readpage call scheduled precheck of finaltup for
+				 * required scan keys up to and including a > or >= scan key
+				 * (necessary because > and >= are only generally considered
+				 * required when scanning backwards)
+				 */
+				Assert(so->scanBehind);
+				so->oppositeDirCheck = false;
+				if (!_bt_oppodir_checkkeys(scan, dir, pstate.finaltup))
+				{
+					/*
+					 * Back out of continuing with this leaf page -- schedule
+					 * another primitive index scan after all
+					 */
+					so->currPos.moreRight = false;
+					so->needPrimScan = true;
+					return false;
+				}
+			}
 		}
 
 		/* load items[] in ascending order */
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index b4ba51357..75608034d 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -1371,7 +1371,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 			curArrayKey->cur_elem = 0;
 		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
 	}
-	so->scanBehind = false;
+	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
 /*
@@ -1680,8 +1680,7 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
 
 	Assert(so->numArrayKeys);
 
-	/* scanBehind flag doesn't persist across primitive index scans - reset */
-	so->scanBehind = false;
+	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 
 	/*
 	 * Array keys are advanced within _bt_checkkeys when the scan reaches the
@@ -1817,7 +1816,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 
-		so->scanBehind = false; /* reset */
+		so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 
 		/*
 		 * Required scan key wasn't satisfied, so required arrays will have to
@@ -2302,19 +2301,27 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	if (so->scanBehind && has_required_opposite_direction_only)
 	{
 		/*
-		 * However, we avoid this behavior whenever the scan involves a scan
+		 * However, we do things differently whenever the scan involves a scan
 		 * key required in the opposite direction to the scan only, along with
 		 * a finaltup with at least one truncated attribute that's associated
 		 * with a scan key marked required (required in either direction).
 		 *
 		 * _bt_check_compare simply won't stop the scan for a scan key that's
 		 * marked required in the opposite scan direction only.  That leaves
-		 * us without any reliable way of reconsidering any opposite-direction
+		 * us without an automatic way of reconsidering any opposite-direction
 		 * inequalities if it turns out that starting a new primitive index
 		 * scan will allow _bt_first to skip ahead by a great many leaf pages
 		 * (see next section for details of how that works).
+		 *
+		 * We deal with this by explicitly scheduling a finaltup recheck for
+		 * the next page -- we'll call _bt_oppodir_checkkeys for the next
+		 * page's finaltup instead.  You can think of this as a way of dealing
+		 * with this page's finaltup being truncated by checking the next
+		 * page's finaltup instead.  And you can think of the oppositeDirCheck
+		 * recheck handling within _bt_readpage as complementing the similar
+		 * scanBehind recheck made from within _bt_checkkeys.
 		 */
-		goto new_prim_scan;
+		so->oppositeDirCheck = true;	/* schedule next page's finaltup recheck */
 	}
 
 	/*
@@ -2352,54 +2359,16 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * also before the _bt_first-wise start of tuples for our new qual.  That
 	 * at least suggests many more skippable pages beyond the current page.
 	 */
-	if (has_required_opposite_direction_only && pstate->finaltup &&
-		(all_required_satisfied || oppodir_inequality_sktrig))
+	else if (has_required_opposite_direction_only && pstate->finaltup &&
+			 (all_required_satisfied || oppodir_inequality_sktrig) &&
+			 unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup)))
 	{
-		int			nfinaltupatts = BTreeTupleGetNAtts(pstate->finaltup, rel);
-		ScanDirection flipped;
-		bool		continuescanflip;
-		int			opsktrig;
-
 		/*
-		 * We're checking finaltup (which is usually not caller's tuple), so
-		 * cannot reuse work from caller's earlier _bt_check_compare call.
-		 *
-		 * Flip the scan direction when calling _bt_check_compare this time,
-		 * so that it will set continuescanflip=false when it encounters an
-		 * inequality required in the opposite scan direction.
+		 * Make sure that any non-required arrays are set to the first array
+		 * element for the current scan direction
 		 */
-		Assert(!so->scanBehind);
-		opsktrig = 0;
-		flipped = -dir;
-		_bt_check_compare(scan, flipped,
-						  pstate->finaltup, nfinaltupatts, tupdesc,
-						  false, false, false,
-						  &continuescanflip, &opsktrig);
-
-		/*
-		 * Only start a new primitive index scan when finaltup has a required
-		 * unsatisfied inequality (unsatisfied in the opposite direction)
-		 */
-		Assert(all_required_satisfied != oppodir_inequality_sktrig);
-		if (unlikely(!continuescanflip &&
-					 so->keyData[opsktrig].sk_strategy != BTEqualStrategyNumber))
-		{
-			/*
-			 * It's possible for the same inequality to be unsatisfied by both
-			 * caller's tuple (in scan's direction) and finaltup (in the
-			 * opposite direction) due to _bt_check_compare's behavior with
-			 * NULLs
-			 */
-			Assert(opsktrig >= sktrig); /* not opsktrig > sktrig due to NULLs */
-
-			/*
-			 * Make sure that any non-required arrays are set to the first
-			 * array element for the current scan direction
-			 */
-			_bt_rewind_nonrequired_arrays(scan, dir);
-
-			goto new_prim_scan;
-		}
+		_bt_rewind_nonrequired_arrays(scan, dir);
+		goto new_prim_scan;
 	}
 
 	/*
@@ -3511,7 +3480,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 *
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
-		Assert(!so->scanBehind && !pstate->prechecked && !pstate->firstmatch);
+		Assert(!so->scanBehind && !so->oppositeDirCheck);
+		Assert(!pstate->prechecked && !pstate->firstmatch);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
@@ -3623,6 +3593,49 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 								  ikey, true);
 }
 
+/*
+ * Test whether an indextuple satisfies inequalities required in the opposite
+ * direction only (and lower-order equalities required in either direction).
+ *
+ * scan: index scan descriptor (containing a search-type scankey)
+ * dir: current scan direction (flipped by us to get opposite direction)
+ * finaltup: final index tuple on the page
+ *
+ * Caller's finaltup tuple is the page high key (for forwards scans), or the
+ * first non-pivot tuple (for backwards scans).  Called during scans with
+ * required array keys.
+ *
+ * Return true if finatup satisfies keys, false if not.  If the tuple fails to
+ * pass the qual, then caller should start another primitive index scan;
+ * _bt_first can efficiently relocate the scan to a far later leaf page.
+ *
+ * Note: we focus on required-in-opposite-direction scan keys (e.g. for a
+ * required > or >= key, assuming a forwards scan) because _bt_checkkeys() can
+ * always deal with required-in-current-direction scan keys on its own.
+ */
+bool
+_bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+					  IndexTuple finaltup)
+{
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	int			nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+	bool		continuescan;
+	ScanDirection flipped = -dir;
+	int			ikey = 0;
+
+	Assert(so->numArrayKeys);
+
+	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
+					  false, false, false, &continuescan, &ikey);
+
+	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
+		return false;
+
+	return true;
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
-- 
2.45.2

In reply to: Peter Geoghegan (#38)
2 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Sep 25, 2024 at 3:08 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v9.

I think that v9-0002-Normalize-nbtree-truncated-high-key-array-behavio.patch
is close to committable. It's basically independent work, which would
be nice to get out of the way soon.

Committed. Thanks for the review, Tomas.

Attached is v10, which is another revision that's intended only to fix
bit rot against the master branch. There are no notable changes to
report.

--
Peter Geoghegan

Attachments:

v10-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v10-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From 8a2d9f4490bce8bad02d0483970f0dd0638247e5 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v10 1/2] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 doc/src/sgml/bloom.sgml                       |   6 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 21 files changed, 242 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 114a85dc4..361c33fca 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -131,6 +131,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index c0b978119..52939d317 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -585,6 +585,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index f2fd62afb..5e423e155 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index b35b8a975..36f1435cb 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index 0d99d6abc..927ba1039 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 69c360843..3b62449d3 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 9b968aa0f..ddc6e1f7a 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -70,6 +70,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -552,6 +553,7 @@ btinitparallelscan(void *target)
 	SpinLockInit(&bt_target->btps_mutex);
 	bt_target->btps_scanPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -577,6 +579,7 @@ btparallelrescan(IndexScanDesc scan)
 	SpinLockAcquire(&btscan->btps_mutex);
 	btscan->btps_scanPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -687,6 +690,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			*pageno = btscan->btps_scanPage;
 			exit_loop = true;
@@ -768,6 +776,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -801,6 +811,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 	{
 		btscan->btps_scanPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 91ac6533f..1bc21eb50 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -963,6 +963,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 301786185..be668abf2 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 18a5af6b9..767428227 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -89,6 +90,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2087,6 +2089,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2100,6 +2103,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2116,6 +2120,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2634,6 +2639,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 19f2b172c..92b13f539 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -170,9 +170,10 @@ CREATE INDEX
    Heap Blocks: exact=28
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..1792.00 rows=2 width=0) (actual time=0.356..0.356 rows=29 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 0.408 ms
-(8 rows)
+(9 rows)
 </programlisting>
   </para>
 
@@ -202,11 +203,12 @@ CREATE INDEX
    -&gt;  BitmapAnd  (cost=24.34..24.34 rows=2 width=0) (actual time=0.027..0.027 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..12.04 rows=500 width=0) (actual time=0.026..0.026 rows=0 loops=1)
                Index Cond: (i5 = 123451)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
-(9 rows)
+(10 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 331315f8d..014a66ef7 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4180,12 +4180,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index ff689b652..1f2172960 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -702,8 +702,10 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Heap Blocks: exact=10
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 1
  Planning Time: 0.485 ms
  Execution Time: 0.073 ms
 </screen>
@@ -754,6 +756,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Heap Blocks: exact=90
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
  Planning Time: 0.187 ms
  Execution Time: 3.036 ms
 </screen>
@@ -819,6 +822,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -848,9 +852,11 @@ EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique
          Buffers: shared hit=4 read=3
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared hit=2
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
                Buffers: shared hit=2 read=3
  Planning:
    Buffers: shared hit=3
@@ -883,6 +889,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Heap Blocks: exact=90
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1017,6 +1024,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
  Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
  Planning Time: 0.077 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a854..e042638b7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -502,9 +502,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    Batches: 1  Memory Usage: 24kB
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(7 rows)
+(8 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index ae9ce9d8e..c24d56007 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index f6b8329cd..a8bc0bfd7 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7a03b4e36..e3e99272a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2657,47 +2661,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off)
@@ -2713,16 +2726,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2741,7 +2757,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off)
@@ -2757,16 +2773,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2787,7 +2806,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2877,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2897,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,17 +2986,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2982,17 +3013,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3064,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3048,17 +3091,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3112,17 +3161,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3144,17 +3199,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3482,12 +3543,14 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(15);
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3566,10 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(25);
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3617,17 @@ explain (analyze, costs off, summary off, timing off) select * from ma_test wher
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4197,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 33a6dceb0..b7cf35b9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index 2eaeb1477..9afe205e0 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 442428d93..085e746af 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

v10-0002-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v10-0002-Add-skip-scan-to-nbtree.patchDownload
From bc36c046ee40532f17d3419abea9dbac15ba836d Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v10 2/2] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
useful in cases where the total number of distinct values in the column
'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.

The design of skip arrays allows array advancement to work without
concern for which specific array types are involved; only the lowest
level code needs to treat skip arrays and SAOP arrays differently.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values are conceptually just like any
other value that might appear in either kind of array.  A call made to
_bt_first to reposition the scan based on a NEXT/PRIOR sentinel usually
won't locate a leaf page containing tuples that must be returned to the
scan, but that's normal during any skip scan that happens to involve
"sparse" indexed values (skip support can only help with "dense" indexed
values, which isn't meaningfully possible when the skipped column is of
a continuous type, where skip support typically can't be implemented).

The optimizer doesn't use distinct new index paths to represent index
skip scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  Skipping is possible during
eligible bitmap index scans, index scans, and index-only scans.  It's
also possible during eligible parallel scans.  An eligible scan is any
scan that nbtree preprocessing generates a skip array for, which happens
whenever doing so will enable later preprocessing to mark original input
scan keys (passed by the executor) as required to continue the scan.

There are hardly any limitations around where skip arrays/scan keys may
appear relative to conventional/input scan keys.  This is no less true
in the presence of conventional SAOP array scan keys, which may both
roll over and be rolled over by skip arrays.  For example, a skip array
on the column "b" is generated for "WHERE a = 42 AND c IN (5, 6, 7, 8)".
As always (since commit 5bf748b8), whether or not nbtree actually skips
depends in large part on physical index characteristics at runtime.
Preprocessing is optimistic about skipping working out: it applies
simple static rules to decide where to place skip arrays.  It's up to
array-related scan code to keep the overhead of maintaining the scan's
skip arrays under control when it turns out that skipping isn't helpful.

Preprocessing will never cons up a skip array for an index column that
has an equality strategy scan key on input, but will do so for indexed
columns that are only constrained by inequality strategy scan keys.
This allows the scan to skip using a range skip array.  These are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   28 +-
 src/include/catalog/pg_amproc.dat             |   16 +
 src/include/catalog/pg_proc.dat               |   24 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  109 ++
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  273 +++
 src/backend/access/nbtree/nbtree.c            |  208 ++-
 src/backend/access/nbtree/nbtsearch.c         |   84 +-
 src/backend/access/nbtree/nbtutils.c          | 1468 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   46 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  380 +++--
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/uuid.c                  |   71 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/btree.sgml                       |   32 +
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   49 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |   10 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   61 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    5 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 34 files changed, 2717 insertions(+), 318 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c51de742e..0826c124f 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -202,7 +202,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 90a68375a..6372d2aee 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1031,10 +1033,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* skip scan support (unless !use_sksup) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo   *low_order;		/* low_compare's ORDER proc */
+	FmgrInfo   *high_order;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1124,6 +1138,10 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on omitted prefix column */
+#define SK_BT_NEGPOSINF	0x00080000	/* -inf/+inf key (invalid sk_argument) */
+#define SK_BT_NEXT		0x00100000	/* positions scan > sk_argument */
+#define SK_BT_PRIOR		0x00200000	/* positions scan < sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1160,6 +1178,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1170,7 +1192,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5d7fe292b..c0ccdfdfb 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -122,6 +128,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +149,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +170,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +205,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +275,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7c0b74fe0..f04a26622 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2257,6 +2272,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4444,6 +4462,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -9320,6 +9341,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d70e6d37e..5b58739ad 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..bd1fb599a
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,109 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining pairs of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code can fall back on next-key sentinel values for any opclass
+ * that doesn't provide its own skip support function.  There is no point in
+ * providing skip support unless the next indexed key value is often the next
+ * indexable value (at least with some workloads).  Opclasses where that never
+ * works out in practice should just rely on the B-Tree AM's generic next-key
+ * fallback strategy.  Opclasses where adding skip support is infeasible or
+ * hard (e.g., an opclass for a continuous type) can also use the fallback.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order).
+	 * This gives the B-Tree code a useful value to start from, before any
+	 * data has been read from the index.
+	 *
+	 * low_elem and high_elem are also used by skip scans to determine when
+	 * they've reached the final possible value (in the current direction).
+	 * It's typical for the scan to run out of leaf pages before it runs out
+	 * of unscanned indexable values, but it's still useful for the scan to
+	 * have a way to recognize when it has reached the last possible value
+	 * (it'll sometimes save a useless probe for a lesser/greater value).
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (in the case of pass-by-reference
+	 * types).  It's not okay for these functions to leak any memory.
+	 *
+	 * Both decrement and increment callbacks are guaranteed to never be
+	 * called with a NULL "existing" arg.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree skip scan caller's "existing" datum is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 1859be614..b4f1466d4 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..26ebbf776 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index ddc6e1f7a..abf168acd 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -32,6 +32,7 @@
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
 #include "storage/smgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -71,7 +72,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -79,11 +80,23 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -536,10 +549,156 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
 	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Assume every index attribute might require that we generate a skip scan
+	 * key
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		Form_pg_attribute attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array/skip scan key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array/skip scan key, while freeing memory allocated
+		 * for old sk_argument where required
+		 */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -550,7 +709,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_scanPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	bt_target->btps_nsearches = 0;
@@ -572,15 +732,15 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_scanPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -607,6 +767,7 @@ btparallelrescan(IndexScanDesc scan)
 bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false;
 	bool		status = true;
@@ -640,7 +801,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -655,14 +816,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				*pageno = InvalidBlockNumber;
 				exit_loop = true;
 			}
@@ -699,7 +855,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *pageno, bool first)
 			*pageno = btscan->btps_scanPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -729,10 +885,10 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber scan_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_scanPage = scan_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -769,7 +925,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -778,7 +934,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -796,6 +952,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -805,7 +962,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_scanPage == prev_scan_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -814,14 +971,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber prev_scan_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 1bc21eb50..7bc148e4c 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -883,7 +883,6 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	Buffer		buf;
 	BTStack		stack;
 	OffsetNumber offnum;
-	StrategyNumber strat;
 	BTScanInsertData inskey;
 	ScanKey		startKeys[INDEX_MAX_KEYS];
 	ScanKeyData notnullkeys[INDEX_MAX_KEYS];
@@ -975,7 +974,17 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * a > or < boundary or find an attribute with no boundary (which can be
 	 * thought of as the same as "> -infinity"), we can't use keys for any
 	 * attributes to its right, because it would break our simplistic notion
-	 * of what initial positioning strategy to use.
+	 * of what initial positioning strategy to use.  In practice skip scan
+	 * typically enables us to use all scan keys here, even with a set of
+	 * input keys that leave a "gap" between two index attributes (cases with
+	 * multiple gaps will even manage this without any special restrictions).
+	 *
+	 * Skip scan works by having _bt_preprocess_keys cons up = boundary keys
+	 * for any index columns that were missing a = key in scan->keyData[], the
+	 * input scan keys passed to us by the executor.  This happens for index
+	 * attributes prior to the attribute of our final input scan key.  The
+	 * underlying = keys use skip arrays.  A skip array key on the column x
+	 * can be thought of as "x = ANY('{every possible x value}')".
 	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
@@ -1050,6 +1059,35 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		{
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					/* -inf/+inf element from a skip array's scan key */
+					ScanKey		skiparraysk = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == skiparraysk - so->keyData)
+							break;
+					}
+
+					/* use array's inequality key in startKeys[] */
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					/*
+					 * If the skip array doesn't include a NULL element, it
+					 * implies a NOT NULL constraint
+					 */
+					if (!array->null_elem)
+						impliesNN = skiparraysk;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
 				/*
 				 * Done looking at keys for curattr.  If we didn't find a
 				 * usable boundary key, see if we can deduce a NOT NULL key.
@@ -1083,16 +1121,48 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 				startKeys[keysz++] = chosen;
 
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					/*
+					 * Next/prior key element from a skip array's scan key.
+					 * Adjust strat_total, so that our = key gets treated like
+					 * a > key (or like a < key) within _bt_search.
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).
+					 */
+					Assert(strat_total == BTEqualStrategyNumber);
+					Assert(!(chosen->sk_flags & SK_SEARCHNOTNULL));
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[]
+					 */
+					break;
+				}
+
 				/*
 				 * Adjust strat_total, and quit if we have stored a > or <
 				 * key.
 				 */
-				strat = chosen->sk_strategy;
-				if (strat != BTEqualStrategyNumber)
+				if (chosen->sk_strategy != BTEqualStrategyNumber)
 				{
-					strat_total = strat;
-					if (strat == BTGreaterStrategyNumber ||
-						strat == BTLessStrategyNumber)
+					strat_total = chosen->sk_strategy;
+					if (chosen->sk_strategy == BTGreaterStrategyNumber ||
+						chosen->sk_strategy == BTLessStrategyNumber)
 						break;
 				}
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 61ded00ca..972ccc90f 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	Oid			eq_op;			/* = op to be used to add array, if any */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -64,15 +92,37 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno,
+							BTSkipPreproc *skipatts);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -256,6 +306,12 @@ _bt_freestack(BTStack stack)
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -271,10 +327,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numArrayKeyData,
+				numSkipArrayKeys;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -282,11 +340,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
+	Assert(scan->numberOfKeys);
 
-	/* Quick check to see if there are any array keys */
+	/*
+	 * Quick check to see if there are any array keys, or any missing keys we
+	 * can generate a "skip scan" array key for ourselves
+	 */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	numArrayKeyData = scan->numberOfKeys;	/* Initial arrayKeyData[] size */
+	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
 		cur = &scan->keyData[i];
 		if (cur->sk_flags & SK_SEARCHARRAY)
@@ -302,6 +364,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		}
 	}
 
+	numSkipArrayKeys = _bt_decide_skipatts(scan, skipatts);
+	if (numSkipArrayKeys)
+	{
+		/* At least one skip array must be added to so->arrayKeys[] */
+		numArrayKeys += numSkipArrayKeys;
+		/* Associated scan keys must be added to arrayKeyData[], too */
+		numArrayKeyData += numSkipArrayKeys;
+	}
+
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
@@ -320,17 +391,23 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/*
+	 * Process each array key, and generate skip arrays as needed.  Also copy
+	 * every scan->keyData[] input scan key (whether it's an array or not)
+	 * into the arrayKeyData array we'll return to our caller (barring any
+	 * array scan keys that we could eliminate early through array merging).
+	 */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -346,16 +423,82 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and scan key where indicated by skipatts */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* won't skip using this attribute */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[numArrayKeyData];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].low_order = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].high_order = NULL;	/* for now */
+
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how the
+			 * consed-up "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			numArrayKeyData++;	/* keep this scan key/array */
+		}
+
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
+		cur = &arrayKeyData[numArrayKeyData];
 		*cur = scan->keyData[input_ikey];
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
@@ -414,7 +557,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -425,7 +568,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -442,7 +585,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -517,17 +660,23 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].low_order = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].high_order = NULL;	/* unused */
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -633,7 +782,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -684,7 +834,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -694,6 +844,191 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_decide_skipatts() -- set index attributes requiring skip arrays
+ *
+ * _bt_preprocess_array_keys helper function.  Determines which attributes
+ * will require skip arrays/scan keys.  Also sets up skip support callbacks
+ * for attributes whose input opclass have skip support (opclasses without
+ * skip support will fall back on using next-key sentinel values when
+ * advancing the skip array to its next array element).
+ *
+ * Return value is the total number of scan keys to add as "input" scan keys
+ * for further processing within _bt_preprocess_keys.
+ */
+static int
+_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_inkey = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+		int			prev_numSkipArrayKeys = numSkipArrayKeys;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				return prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			numSkipArrayKeys++;
+			attno_skip++;
+		}
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				numSkipArrayKeys++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				return numSkipArrayKeys;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	return numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatts
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatts)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatts->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										  BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack even an equality strategy
+	 * operator, but they're supported.  We cope by making caller not use a
+	 * skip array for this index column (nor for any lower-order columns).
+	 */
+	if (!OidIsValid(skipatts->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatts->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+														reverse,
+														&skipatts->sksup);
+
+	/* might not have set up skip support, but can skip either way */
+	return true;
+}
+
 /*
  * _bt_setup_array_cmp() -- Set up array comparison functions
  *
@@ -980,26 +1315,28 @@ _bt_merge_arrays(IndexScanDesc scan, ScanKey skey, FmgrInfo *sortproc,
  * guaranteeing that at least the scalar scan key can be considered redundant.
  * We return false if the comparison could not be made (caller must keep both
  * scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar inequality scan keys.
  */
 static bool
 _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
 	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
 		   skey->sk_strategy != BTEqualStrategyNumber);
@@ -1009,8 +1346,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.  We have to be sure
+	 * that _bt_compare_array_skey/_bt_binsrch_array_skey use the right proc.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1041,11 +1378,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensures that the scalar scan key can be eliminated as redundant
+		 */
+		eliminated = true;
+
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+	}
+	else
+	{
+		/*
+		 * With a skip array, it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when there's a lack of suitable cross-type support for
+		 * comparing skey to an existing inequality associated with the array.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when it
+ * is redundant with (or contradicted by) a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1097,6 +1488,121 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of skip array scan key when it is "redundant with"
+ * a non-array scalar scan key.  The scalar scan key must be an inequality.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array, forcing its elements to be generated within the limits of a range.
+ *
+ * Return value indicates if the scalar scan key could be "eliminated".  We
+ * return true in the common case where caller's scan key was successfully
+ * rolled into the skip array.  We return false when we can't do that due to
+ * the presence of an existing, conflicting inequality that cannot be compared
+ * to caller's inequality due to a lack of suitable cross-type support.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way scan code can assume that it'll always
+	 * be safe to use the same-type ORDER procs stored in so->orderProcs[],
+	 * provided the array's scan key isn't marked NEGPOSINF.  When the array's
+	 * scan key is marked NEGPOSINF, then it'll lack a valid sk_argument, and
+	 * it'll be necessary to apply low_compare and/or high_compare separately.
+	 *
+	 * Under this scheme, there isn't any danger of anybody becoming confused
+	 * about which type is used where, even in cross-type scenarios; each scan
+	 * key consistently uses the same underlying type.  While NEGPOSINF is a
+	 * distinct array element, it is only used to force low_compare and/or
+	 * high_compare to find the real first (or final) element in the index.
+	 * You can think of NEGPOSINF as representing an imaginary point in the
+	 * key space immediately before the start (or immediately after the end)
+	 * of the true first (or true final) matching value.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare */
+
+				/* replace old high_compare with new one */
+			}
+			else
+			{
+				/* Allocate scan key/ORDER proc buffers in so->arrayContext */
+				array->high_compare = palloc(sizeof(ScanKeyData));
+				array->high_order = palloc(sizeof(FmgrInfo));
+			}
+
+			*array->high_compare = *skey;
+			*array->high_order = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare */
+
+				/* replace old low_compare with new one */
+			}
+			else
+			{
+				/* Allocate scan key/ORDER proc buffers in so->arrayContext */
+				array->low_compare = palloc(sizeof(ScanKeyData));
+				array->low_order = palloc(sizeof(FmgrInfo));
+			}
+
+			*array->low_compare = *skey;
+			*array->low_order = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
@@ -1220,6 +1726,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1342,6 +1850,98 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, because the array doesn't really
+ * have any elements (it generates its array elements procedurally instead).
+ * Note that this may include a NULL value/an IS NULL qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ *
+ * Note: This function doesn't actually binary search.  It uses an interface
+ * that's as similar to _bt_binsrch_array_skey as possible for consistency.
+ * "Binary searching a skip array" just means determining if tupdatum/tupnull
+ * are before, within, or beyond the range of caller's skip array.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1351,29 +1951,486 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting skip array to lowest element, which isn't just NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Setting skip array to highest element, which isn't just NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		underflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value can only be
+	 * determined when the scan reads lower sorting tuples.
+	 *
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, _bt_first can find the highest non-NULL.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * low_compare is for an >= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true prior value
+		 * cannot possibly satisfy low_compare.  We can give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(array->low_order,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true prior value */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &underflow);
+
+	if (underflow)
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the skip array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		overflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value can only be
+	 * determined when the scan reads higher sorting tuples.
+	 *
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, _bt_first can find the lowest non-NULL.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * high_compare is for an <= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true next value cannot
+		 * possibly satisfy high_compare.  We can give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(array->high_order,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true next value */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &overflow);
+
+	if (overflow)
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the skip array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1389,6 +2446,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1398,29 +2456,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1475,6 +2534,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1482,7 +2542,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1494,16 +2553,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1628,9 +2681,94 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_NEGPOSINF))
+		{
+			/* Scankey has a valid/comparable sk_argument value (not -inf) */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXT))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument + infinitesimal"
+				 */
+				if (ScanDirectionIsForward(dir))
+				{
+					/* tupdatum is still < "sk_argument + infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was forward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to backward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+			else if (result == 0 && (cur->sk_flags & SK_BT_PRIOR))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument - infinitesimal"
+				 */
+				if (ScanDirectionIsBackward(dir))
+				{
+					/* tupdatum is still > "sk_argument - infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was backward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to forward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+		}
+		else
+		{
+			/*
+			 * Scankey searches for the sentinel value -inf (or +inf).
+			 *
+			 * Note: -inf could mean "absolute" -inf, or it could represent an
+			 * imaginary point in the key space that comes immediately before
+			 * the first real value that satisfies the array's low_compare.
+			 * +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * Compare tupdatum against -inf using array's low_compare, if any
+			 * (or compare it against +inf using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is > -inf sk_argument (or < +inf sk_argument).
+				 * It's time for caller to advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1962,18 +3100,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1998,18 +3127,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2027,12 +3147,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -2108,11 +3237,62 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  This is often the range -inf through to +inf.
+		 * "Binary searching" only determined whether tupdatum is beyond,
+		 * before, or within the range of the skip array.
+		 *
+		 * As always, we set the array element to its closest available match.
+		 * But unlike with a conventional array, a skip array's new element
+		 * might be NEGPOSINF, a special sentinel value.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == "some array element".
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2458,6 +3638,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also cons up skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2470,10 +3652,16 @@ end_toplevel_scan:
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never consed up skip array scan keys, it would be possible for "gaps"
+ * to appear that made it unsafe to mark any subsequent input scan keys (taken
+ * from scan->keyData[]) as required to continue the scan.  If there are no
+ * keys for a given attribute, the keys for subsequent attributes can never be
+ * required.  For instance, "WHERE y = 4" always required a full-index scan on
+ * version prior to Postgres 18.  Now preprocessing rewrites such a qual into
+ * "WHERE x = ANY('{every possible x value}') and y = 4", thereby enabling
+ * marking the key on y (and the key on x) required to continue te scan.
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -2510,7 +3698,12 @@ end_toplevel_scan:
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That won't work with skip arrays, which work by making a value into the
+ * current array element, which anchors later scan keys via "=" comparisons.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -2574,6 +3767,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -2713,7 +3914,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
+						Assert(!array || array->num_elems > 0 ||
+							   array->num_elems == -1);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -2761,6 +3963,16 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * Our call to _bt_preprocess_array_keys is generally expected to
+			 * have already added "=" scan keys with skip arrays as required
+			 * to form an unbroken series of "=" constraints on all attrs
+			 * prior to the attr from the last scan key that came from our
+			 * original set of scan->keyData[] input scan keys.  Despite all
+			 * this, we cannot assume _bt_preprocess_array_keys will always
+			 * make it safe for us to mark all scan keys required.  Certain
+			 * corner cases might have prevented it from adding a skip array
+			 * (e.g., opclasses without an "=" operator can't use "=" arrays).
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -2849,6 +4061,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -2857,6 +4070,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -2876,7 +4090,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
+					Assert(!array || array->num_elems > 0 ||
+						   array->num_elems == -1);
 
 					/*
 					 * New key is more restrictive, and so replaces old key...
@@ -3018,10 +4233,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3087,6 +4303,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -3096,6 +4315,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3169,6 +4404,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3730,6 +4966,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index db6ed784a..86a5de8f4 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 8efb4044d..6efa3e353 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -372,6 +372,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 85e5eaf32..88c929a0a 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -98,6 +98,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 8130f3e8a..256350007 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f73f294b8..cb8470324 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -85,6 +85,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 08fa6774d..d04bd379d 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -192,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -213,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5736,6 +5740,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6794,6 +6884,54 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a
+ * single-column index, or C * 0.75 for multiple columns. (The idea here
+ * is that multiple columns dilute the importance of the first column's
+ * ordering, but don't negate it entirely.  Before 8.0 we divided the
+ * correlation by the number of columns, but that seems too strong.)
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6803,17 +6941,22 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6829,14 +6972,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -6846,13 +6994,90 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't absord a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6874,6 +7099,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6894,7 +7120,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6910,6 +7136,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6925,6 +7183,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -7033,104 +7292,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 5284d23dc..654bda638 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -384,6 +387,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 2c4cc8cd4..90de15c4b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1751,6 +1752,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3590,6 +3602,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..d256e091f 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index d3358dfc3..fa6f27e6d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1938,7 +1938,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -1958,7 +1958,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 756c2e249..564c1fe4b 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4370,24 +4370,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -7492,19 +7493,23 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                        QUERY PLAN                         
------------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Nested Loop
    ->  Hash Join
          Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-         ->  Seq Scan on fkest f2
-               Filter: (x100 = 2)
+         ->  Bitmap Heap Scan on fkest f2
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
          ->  Hash
-               ->  Seq Scan on fkest f1
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f1
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
          Index Cond: (x = f1.x)
-(10 rows)
+(14 rows)
 
 alter table fkest add constraint fk
   foreign key (x, x10b, x100) references fkest (x, x10, x100);
@@ -7513,20 +7518,24 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                     QUERY PLAN                      
------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Hash Join
    Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
    ->  Hash Join
          Hash Cond: (f3.x = f2.x)
          ->  Seq Scan on fkest f3
          ->  Hash
-               ->  Seq Scan on fkest f2
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f2
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Hash
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-(11 rows)
+         ->  Bitmap Heap Scan on fkest f1
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
+(15 rows)
 
 rollback;
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 3819bf5e2..58c2d013a 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5240,9 +5240,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index c73631a9a..62eb4239a 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1461,18 +1461,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index fe162cc7c..dea40e013 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -766,7 +766,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -776,7 +776,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 57de1acff..c0b621f40 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2662,6 +2663,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

In reply to: Peter Geoghegan (#39)
2 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Oct 16, 2024 at 1:14 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v10, which is another revision that's intended only to fix
bit rot against the master branch. There are no notable changes to
report.

Attached is v11, which is yet another revision whose sole purpose is
to fix bit rot/make the patch apply cleanly against the master
branch's tip.

This time the bit rot is caused by independent nbtree work, on
backwards scans. Nothing interesting here compared to v10 (or to v9).

--
Peter Geoghegan

Attachments:

v11-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/x-patch; name=v11-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From 761e8e593a2accb6ff01969a240d83c5da686687 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v11 1/2] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 doc/src/sgml/bloom.sgml                       |   6 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 21 files changed, 242 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 114a85dc4..361c33fca 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -131,6 +131,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index c0b978119..52939d317 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -585,6 +585,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index f2fd62afb..5e423e155 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index b35b8a975..36f1435cb 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index 0d99d6abc..927ba1039 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 69c360843..3b62449d3 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index f4f79f270..1b5683d7e 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -72,6 +72,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -555,6 +556,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -581,6 +583,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -703,6 +706,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			*next_scan_page = btscan->btps_nextScanPage;
 			*last_curr_page = btscan->btps_lastCurrPage;
@@ -790,6 +798,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -824,6 +834,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 2275553be..cf72378fc 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -971,6 +971,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 301786185..be668abf2 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 18a5af6b9..767428227 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -89,6 +90,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2087,6 +2089,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2100,6 +2103,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2116,6 +2120,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2634,6 +2639,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 19f2b172c..92b13f539 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -170,9 +170,10 @@ CREATE INDEX
    Heap Blocks: exact=28
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..1792.00 rows=2 width=0) (actual time=0.356..0.356 rows=29 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 0.408 ms
-(8 rows)
+(9 rows)
 </programlisting>
   </para>
 
@@ -202,11 +203,12 @@ CREATE INDEX
    -&gt;  BitmapAnd  (cost=24.34..24.34 rows=2 width=0) (actual time=0.027..0.027 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..12.04 rows=500 width=0) (actual time=0.026..0.026 rows=0 loops=1)
                Index Cond: (i5 = 123451)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
-(9 rows)
+(10 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 331315f8d..014a66ef7 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4180,12 +4180,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index ff689b652..1f2172960 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -702,8 +702,10 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Heap Blocks: exact=10
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 1
  Planning Time: 0.485 ms
  Execution Time: 0.073 ms
 </screen>
@@ -754,6 +756,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Heap Blocks: exact=90
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
  Planning Time: 0.187 ms
  Execution Time: 3.036 ms
 </screen>
@@ -819,6 +822,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -848,9 +852,11 @@ EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique
          Buffers: shared hit=4 read=3
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared hit=2
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
                Buffers: shared hit=2 read=3
  Planning:
    Buffers: shared hit=3
@@ -883,6 +889,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Heap Blocks: exact=90
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1017,6 +1024,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
  Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
  Planning Time: 0.077 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a854..e042638b7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -502,9 +502,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    Batches: 1  Memory Usage: 24kB
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(7 rows)
+(8 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index ae9ce9d8e..c24d56007 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index f6b8329cd..a8bc0bfd7 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7a03b4e36..e3e99272a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2657,47 +2661,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off)
@@ -2713,16 +2726,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2741,7 +2757,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off)
@@ -2757,16 +2773,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2787,7 +2806,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2877,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2897,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,17 +2986,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2982,17 +3013,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3064,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3048,17 +3091,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3112,17 +3161,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3144,17 +3199,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3482,12 +3543,14 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(15);
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3566,10 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(25);
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3617,17 @@ explain (analyze, costs off, summary off, timing off) select * from ma_test wher
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4197,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 33a6dceb0..b7cf35b9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index 2eaeb1477..9afe205e0 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 442428d93..085e746af 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

v11-0002-Add-skip-scan-to-nbtree.patchapplication/x-patch; name=v11-0002-Add-skip-scan-to-nbtree.patchDownload
From f70008b3c335b06edcc23f32e3f45ed3eb12d793 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v11 2/2] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
useful in cases where the total number of distinct values in the column
'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.

The design of skip arrays allows array advancement to work without
concern for which specific array types are involved; only the lowest
level code needs to treat skip arrays and SAOP arrays differently.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values are conceptually just like any
other value that might appear in either kind of array.  A call made to
_bt_first to reposition the scan based on a NEXT/PRIOR sentinel usually
won't locate a leaf page containing tuples that must be returned to the
scan, but that's normal during any skip scan that happens to involve
"sparse" indexed values (skip support can only help with "dense" indexed
values, which isn't meaningfully possible when the skipped column is of
a continuous type, where skip support typically can't be implemented).

The optimizer doesn't use distinct new index paths to represent index
skip scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  Skipping is possible during
eligible bitmap index scans, index scans, and index-only scans.  It's
also possible during eligible parallel scans.  An eligible scan is any
scan that nbtree preprocessing generates a skip array for, which happens
whenever doing so will enable later preprocessing to mark original input
scan keys (passed by the executor) as required to continue the scan.

There are hardly any limitations around where skip arrays/scan keys may
appear relative to conventional/input scan keys.  This is no less true
in the presence of conventional SAOP array scan keys, which may both
roll over and be rolled over by skip arrays.  For example, a skip array
on the column "b" is generated for "WHERE a = 42 AND c IN (5, 6, 7, 8)".
As always (since commit 5bf748b8), whether or not nbtree actually skips
depends in large part on physical index characteristics at runtime.
Preprocessing is optimistic about skipping working out: it applies
simple static rules to decide where to place skip arrays.  It's up to
array-related scan code to keep the overhead of maintaining the scan's
skip arrays under control when it turns out that skipping isn't helpful.

Preprocessing will never cons up a skip array for an index column that
has an equality strategy scan key on input, but will do so for indexed
columns that are only constrained by inequality strategy scan keys.
This allows the scan to skip using a range skip array.  These are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   28 +-
 src/include/catalog/pg_amproc.dat             |   16 +
 src/include/catalog/pg_proc.dat               |   24 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  109 ++
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  273 +++
 src/backend/access/nbtree/nbtree.c            |  208 ++-
 src/backend/access/nbtree/nbtsearch.c         |   84 +-
 src/backend/access/nbtree/nbtutils.c          | 1468 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   46 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  380 +++--
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/uuid.c                  |   71 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/btree.sgml                       |   32 +
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   49 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |   10 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   61 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    5 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 34 files changed, 2717 insertions(+), 318 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c51de742e..0826c124f 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -202,7 +202,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 5fb523ece..e02332f59 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1022,10 +1024,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* skip scan support (unless !use_sksup) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo   *low_order;		/* low_compare's ORDER proc */
+	FmgrInfo   *high_order;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1114,6 +1128,10 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on omitted prefix column */
+#define SK_BT_NEGPOSINF	0x00080000	/* -inf/+inf key (invalid sk_argument) */
+#define SK_BT_NEXT		0x00100000	/* positions scan > sk_argument */
+#define SK_BT_PRIOR		0x00200000	/* positions scan < sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1150,6 +1168,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1160,7 +1182,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5d7fe292b..c0ccdfdfb 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -122,6 +128,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +149,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +170,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +205,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +275,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7c0b74fe0..f04a26622 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2257,6 +2272,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4444,6 +4462,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -9320,6 +9341,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d70e6d37e..5b58739ad 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..bd1fb599a
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,109 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining pairs of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code can fall back on next-key sentinel values for any opclass
+ * that doesn't provide its own skip support function.  There is no point in
+ * providing skip support unless the next indexed key value is often the next
+ * indexable value (at least with some workloads).  Opclasses where that never
+ * works out in practice should just rely on the B-Tree AM's generic next-key
+ * fallback strategy.  Opclasses where adding skip support is infeasible or
+ * hard (e.g., an opclass for a continuous type) can also use the fallback.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order).
+	 * This gives the B-Tree code a useful value to start from, before any
+	 * data has been read from the index.
+	 *
+	 * low_elem and high_elem are also used by skip scans to determine when
+	 * they've reached the final possible value (in the current direction).
+	 * It's typical for the scan to run out of leaf pages before it runs out
+	 * of unscanned indexable values, but it's still useful for the scan to
+	 * have a way to recognize when it has reached the last possible value
+	 * (it'll sometimes save a useless probe for a lesser/greater value).
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (in the case of pass-by-reference
+	 * types).  It's not okay for these functions to leak any memory.
+	 *
+	 * Both decrement and increment callbacks are guaranteed to never be
+	 * called with a NULL "existing" arg.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree skip scan caller's "existing" datum is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 1859be614..b4f1466d4 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..26ebbf776 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 1b5683d7e..7919f4098 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -32,6 +32,7 @@
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
 #include "storage/smgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -73,7 +74,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -81,11 +82,23 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -538,10 +551,156 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
 	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Assume every index attribute might require that we generate a skip scan
+	 * key
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		Form_pg_attribute attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array/skip scan key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array/skip scan key, while freeing memory allocated
+		 * for old sk_argument where required
+		 */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -552,7 +711,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -575,16 +735,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -613,6 +773,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false;
 	bool		status = true;
@@ -656,7 +817,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -671,14 +832,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				*next_scan_page = InvalidBlockNumber;
 				exit_loop = true;
 			}
@@ -716,7 +872,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -750,11 +906,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -791,7 +947,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -800,7 +956,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -818,6 +974,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -827,7 +984,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -837,14 +994,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index cf72378fc..e59fd9218 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -883,7 +883,6 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	BTStack		stack;
 	OffsetNumber offnum;
-	StrategyNumber strat;
 	BTScanInsertData inskey;
 	ScanKey		startKeys[INDEX_MAX_KEYS];
 	ScanKeyData notnullkeys[INDEX_MAX_KEYS];
@@ -983,7 +982,17 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * a > or < boundary or find an attribute with no boundary (which can be
 	 * thought of as the same as "> -infinity"), we can't use keys for any
 	 * attributes to its right, because it would break our simplistic notion
-	 * of what initial positioning strategy to use.
+	 * of what initial positioning strategy to use.  In practice skip scan
+	 * typically enables us to use all scan keys here, even with a set of
+	 * input keys that leave a "gap" between two index attributes (cases with
+	 * multiple gaps will even manage this without any special restrictions).
+	 *
+	 * Skip scan works by having _bt_preprocess_keys cons up = boundary keys
+	 * for any index columns that were missing a = key in scan->keyData[], the
+	 * input scan keys passed to us by the executor.  This happens for index
+	 * attributes prior to the attribute of our final input scan key.  The
+	 * underlying = keys use skip arrays.  A skip array key on the column x
+	 * can be thought of as "x = ANY('{every possible x value}')".
 	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
@@ -1058,6 +1067,35 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		{
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					/* -inf/+inf element from a skip array's scan key */
+					ScanKey		skiparraysk = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == skiparraysk - so->keyData)
+							break;
+					}
+
+					/* use array's inequality key in startKeys[] */
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					/*
+					 * If the skip array doesn't include a NULL element, it
+					 * implies a NOT NULL constraint
+					 */
+					if (!array->null_elem)
+						impliesNN = skiparraysk;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
 				/*
 				 * Done looking at keys for curattr.  If we didn't find a
 				 * usable boundary key, see if we can deduce a NOT NULL key.
@@ -1091,16 +1129,48 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 				startKeys[keysz++] = chosen;
 
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					/*
+					 * Next/prior key element from a skip array's scan key.
+					 * Adjust strat_total, so that our = key gets treated like
+					 * a > key (or like a < key) within _bt_search.
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).
+					 */
+					Assert(strat_total == BTEqualStrategyNumber);
+					Assert(!(chosen->sk_flags & SK_SEARCHNOTNULL));
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[]
+					 */
+					break;
+				}
+
 				/*
 				 * Adjust strat_total, and quit if we have stored a > or <
 				 * key.
 				 */
-				strat = chosen->sk_strategy;
-				if (strat != BTEqualStrategyNumber)
+				if (chosen->sk_strategy != BTEqualStrategyNumber)
 				{
-					strat_total = strat;
-					if (strat == BTGreaterStrategyNumber ||
-						strat == BTLessStrategyNumber)
+					strat_total = chosen->sk_strategy;
+					if (chosen->sk_strategy == BTGreaterStrategyNumber ||
+						chosen->sk_strategy == BTLessStrategyNumber)
 						break;
 				}
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 76be65123..3faced168 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	Oid			eq_op;			/* = op to be used to add array, if any */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -64,15 +92,37 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno,
+							BTSkipPreproc *skipatts);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -256,6 +306,12 @@ _bt_freestack(BTStack stack)
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -271,10 +327,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numArrayKeyData,
+				numSkipArrayKeys;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -282,11 +340,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
+	Assert(scan->numberOfKeys);
 
-	/* Quick check to see if there are any array keys */
+	/*
+	 * Quick check to see if there are any array keys, or any missing keys we
+	 * can generate a "skip scan" array key for ourselves
+	 */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	numArrayKeyData = scan->numberOfKeys;	/* Initial arrayKeyData[] size */
+	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
 		cur = &scan->keyData[i];
 		if (cur->sk_flags & SK_SEARCHARRAY)
@@ -302,6 +364,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		}
 	}
 
+	numSkipArrayKeys = _bt_decide_skipatts(scan, skipatts);
+	if (numSkipArrayKeys)
+	{
+		/* At least one skip array must be added to so->arrayKeys[] */
+		numArrayKeys += numSkipArrayKeys;
+		/* Associated scan keys must be added to arrayKeyData[], too */
+		numArrayKeyData += numSkipArrayKeys;
+	}
+
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
@@ -320,17 +391,23 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/*
+	 * Process each array key, and generate skip arrays as needed.  Also copy
+	 * every scan->keyData[] input scan key (whether it's an array or not)
+	 * into the arrayKeyData array we'll return to our caller (barring any
+	 * array scan keys that we could eliminate early through array merging).
+	 */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -346,16 +423,82 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and scan key where indicated by skipatts */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* won't skip using this attribute */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[numArrayKeyData];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].low_order = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].high_order = NULL;	/* for now */
+
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how the
+			 * consed-up "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			numArrayKeyData++;	/* keep this scan key/array */
+		}
+
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
+		cur = &arrayKeyData[numArrayKeyData];
 		*cur = scan->keyData[input_ikey];
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
@@ -414,7 +557,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -425,7 +568,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -442,7 +585,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -517,17 +660,23 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].low_order = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].high_order = NULL;	/* unused */
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -633,7 +782,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -684,7 +834,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -694,6 +844,191 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_decide_skipatts() -- set index attributes requiring skip arrays
+ *
+ * _bt_preprocess_array_keys helper function.  Determines which attributes
+ * will require skip arrays/scan keys.  Also sets up skip support callbacks
+ * for attributes whose input opclass have skip support (opclasses without
+ * skip support will fall back on using next-key sentinel values when
+ * advancing the skip array to its next array element).
+ *
+ * Return value is the total number of scan keys to add as "input" scan keys
+ * for further processing within _bt_preprocess_keys.
+ */
+static int
+_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_inkey = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+		int			prev_numSkipArrayKeys = numSkipArrayKeys;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				return prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			numSkipArrayKeys++;
+			attno_skip++;
+		}
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				numSkipArrayKeys++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				return numSkipArrayKeys;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	return numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatts
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatts)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatts->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										  BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack even an equality strategy
+	 * operator, but they're supported.  We cope by making caller not use a
+	 * skip array for this index column (nor for any lower-order columns).
+	 */
+	if (!OidIsValid(skipatts->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatts->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+														reverse,
+														&skipatts->sksup);
+
+	/* might not have set up skip support, but can skip either way */
+	return true;
+}
+
 /*
  * _bt_setup_array_cmp() -- Set up array comparison functions
  *
@@ -980,26 +1315,28 @@ _bt_merge_arrays(IndexScanDesc scan, ScanKey skey, FmgrInfo *sortproc,
  * guaranteeing that at least the scalar scan key can be considered redundant.
  * We return false if the comparison could not be made (caller must keep both
  * scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar inequality scan keys.
  */
 static bool
 _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
 	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
 		   skey->sk_strategy != BTEqualStrategyNumber);
@@ -1009,8 +1346,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.  We have to be sure
+	 * that _bt_compare_array_skey/_bt_binsrch_array_skey use the right proc.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1041,11 +1378,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensures that the scalar scan key can be eliminated as redundant
+		 */
+		eliminated = true;
+
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+	}
+	else
+	{
+		/*
+		 * With a skip array, it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when there's a lack of suitable cross-type support for
+		 * comparing skey to an existing inequality associated with the array.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when it
+ * is redundant with (or contradicted by) a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1097,6 +1488,121 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of skip array scan key when it is "redundant with"
+ * a non-array scalar scan key.  The scalar scan key must be an inequality.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array, forcing its elements to be generated within the limits of a range.
+ *
+ * Return value indicates if the scalar scan key could be "eliminated".  We
+ * return true in the common case where caller's scan key was successfully
+ * rolled into the skip array.  We return false when we can't do that due to
+ * the presence of an existing, conflicting inequality that cannot be compared
+ * to caller's inequality due to a lack of suitable cross-type support.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way scan code can assume that it'll always
+	 * be safe to use the same-type ORDER procs stored in so->orderProcs[],
+	 * provided the array's scan key isn't marked NEGPOSINF.  When the array's
+	 * scan key is marked NEGPOSINF, then it'll lack a valid sk_argument, and
+	 * it'll be necessary to apply low_compare and/or high_compare separately.
+	 *
+	 * Under this scheme, there isn't any danger of anybody becoming confused
+	 * about which type is used where, even in cross-type scenarios; each scan
+	 * key consistently uses the same underlying type.  While NEGPOSINF is a
+	 * distinct array element, it is only used to force low_compare and/or
+	 * high_compare to find the real first (or final) element in the index.
+	 * You can think of NEGPOSINF as representing an imaginary point in the
+	 * key space immediately before the start (or immediately after the end)
+	 * of the true first (or true final) matching value.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare */
+
+				/* replace old high_compare with new one */
+			}
+			else
+			{
+				/* Allocate scan key/ORDER proc buffers in so->arrayContext */
+				array->high_compare = palloc(sizeof(ScanKeyData));
+				array->high_order = palloc(sizeof(FmgrInfo));
+			}
+
+			*array->high_compare = *skey;
+			*array->high_order = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare */
+
+				/* replace old low_compare with new one */
+			}
+			else
+			{
+				/* Allocate scan key/ORDER proc buffers in so->arrayContext */
+				array->low_compare = palloc(sizeof(ScanKeyData));
+				array->low_order = palloc(sizeof(FmgrInfo));
+			}
+
+			*array->low_compare = *skey;
+			*array->low_order = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
@@ -1220,6 +1726,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1342,6 +1850,98 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, because the array doesn't really
+ * have any elements (it generates its array elements procedurally instead).
+ * Note that this may include a NULL value/an IS NULL qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ *
+ * Note: This function doesn't actually binary search.  It uses an interface
+ * that's as similar to _bt_binsrch_array_skey as possible for consistency.
+ * "Binary searching a skip array" just means determining if tupdatum/tupnull
+ * are before, within, or beyond the range of caller's skip array.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1351,29 +1951,486 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting skip array to lowest element, which isn't just NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Setting skip array to highest element, which isn't just NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		underflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value can only be
+	 * determined when the scan reads lower sorting tuples.
+	 *
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, _bt_first can find the highest non-NULL.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * low_compare is for an >= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true prior value
+		 * cannot possibly satisfy low_compare.  We can give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(array->low_order,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true prior value */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &underflow);
+
+	if (underflow)
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the skip array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		overflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value can only be
+	 * determined when the scan reads higher sorting tuples.
+	 *
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, _bt_first can find the lowest non-NULL.
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element will turn out to be out of bounds for the skip
+		 * array.
+		 *
+		 * Skip arrays (that lack skip support) can only do this when their
+		 * high_compare is for an <= inequality; if the current array element
+		 * is == the inequality's sk_argument, then the true next value cannot
+		 * possibly satisfy high_compare.  We can give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(array->high_order,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* else the scan must figure out the true next value */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &overflow);
+
+	if (overflow)
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the skip array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1389,6 +2446,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1398,29 +2456,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1475,6 +2534,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1482,7 +2542,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1494,16 +2553,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1628,9 +2681,94 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_NEGPOSINF))
+		{
+			/* Scankey has a valid/comparable sk_argument value (not -inf) */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXT))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument + infinitesimal"
+				 */
+				if (ScanDirectionIsForward(dir))
+				{
+					/* tupdatum is still < "sk_argument + infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was forward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to backward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+			else if (result == 0 && (cur->sk_flags & SK_BT_PRIOR))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument - infinitesimal"
+				 */
+				if (ScanDirectionIsBackward(dir))
+				{
+					/* tupdatum is still > "sk_argument - infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was backward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to forward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+		}
+		else
+		{
+			/*
+			 * Scankey searches for the sentinel value -inf (or +inf).
+			 *
+			 * Note: -inf could mean "absolute" -inf, or it could represent an
+			 * imaginary point in the key space that comes immediately before
+			 * the first real value that satisfies the array's low_compare.
+			 * +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * Compare tupdatum against -inf using array's low_compare, if any
+			 * (or compare it against +inf using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is > -inf sk_argument (or < +inf sk_argument).
+				 * It's time for caller to advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1962,18 +3100,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1998,18 +3127,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2027,12 +3147,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -2108,11 +3237,62 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  This is often the range -inf through to +inf.
+		 * "Binary searching" only determined whether tupdatum is beyond,
+		 * before, or within the range of the skip array.
+		 *
+		 * As always, we set the array element to its closest available match.
+		 * But unlike with a conventional array, a skip array's new element
+		 * might be NEGPOSINF, a special sentinel value.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == "some array element".
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2458,6 +3638,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also cons up skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2470,10 +3652,16 @@ end_toplevel_scan:
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never consed up skip array scan keys, it would be possible for "gaps"
+ * to appear that made it unsafe to mark any subsequent input scan keys (taken
+ * from scan->keyData[]) as required to continue the scan.  If there are no
+ * keys for a given attribute, the keys for subsequent attributes can never be
+ * required.  For instance, "WHERE y = 4" always required a full-index scan on
+ * version prior to Postgres 18.  Now preprocessing rewrites such a qual into
+ * "WHERE x = ANY('{every possible x value}') and y = 4", thereby enabling
+ * marking the key on y (and the key on x) required to continue te scan.
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -2510,7 +3698,12 @@ end_toplevel_scan:
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That won't work with skip arrays, which work by making a value into the
+ * current array element, which anchors later scan keys via "=" comparisons.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -2574,6 +3767,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -2713,7 +3914,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
+						Assert(!array || array->num_elems > 0 ||
+							   array->num_elems == -1);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -2761,6 +3963,16 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * Our call to _bt_preprocess_array_keys is generally expected to
+			 * have already added "=" scan keys with skip arrays as required
+			 * to form an unbroken series of "=" constraints on all attrs
+			 * prior to the attr from the last scan key that came from our
+			 * original set of scan->keyData[] input scan keys.  Despite all
+			 * this, we cannot assume _bt_preprocess_array_keys will always
+			 * make it safe for us to mark all scan keys required.  Certain
+			 * corner cases might have prevented it from adding a skip array
+			 * (e.g., opclasses without an "=" operator can't use "=" arrays).
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -2849,6 +4061,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -2857,6 +4070,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -2876,7 +4090,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
+					Assert(!array || array->num_elems > 0 ||
+						   array->num_elems == -1);
 
 					/*
 					 * New key is more restrictive, and so replaces old key...
@@ -3018,10 +4233,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3087,6 +4303,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -3096,6 +4315,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3169,6 +4404,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3730,6 +4966,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index db6ed784a..86a5de8f4 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 8efb4044d..6efa3e353 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -372,6 +372,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 85e5eaf32..88c929a0a 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -98,6 +98,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 8130f3e8a..256350007 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f73f294b8..cb8470324 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -85,6 +85,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 08fa6774d..d04bd379d 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -192,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -213,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5736,6 +5740,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6794,6 +6884,54 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a
+ * single-column index, or C * 0.75 for multiple columns. (The idea here
+ * is that multiple columns dilute the importance of the first column's
+ * ordering, but don't negate it entirely.  Before 8.0 we divided the
+ * correlation by the number of columns, but that seems too strong.)
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6803,17 +6941,22 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6829,14 +6972,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -6846,13 +6994,90 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't absord a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6874,6 +7099,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6894,7 +7120,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6910,6 +7136,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6925,6 +7183,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -7033,104 +7292,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 5284d23dc..654bda638 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -384,6 +387,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 2c4cc8cd4..90de15c4b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1751,6 +1752,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3590,6 +3602,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..d256e091f 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index d3358dfc3..fa6f27e6d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1938,7 +1938,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -1958,7 +1958,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 5669ed929..d2f2b65cd 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4425,24 +4425,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -7547,19 +7548,23 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                        QUERY PLAN                         
------------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Nested Loop
    ->  Hash Join
          Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-         ->  Seq Scan on fkest f2
-               Filter: (x100 = 2)
+         ->  Bitmap Heap Scan on fkest f2
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
          ->  Hash
-               ->  Seq Scan on fkest f1
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f1
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
          Index Cond: (x = f1.x)
-(10 rows)
+(14 rows)
 
 alter table fkest add constraint fk
   foreign key (x, x10b, x100) references fkest (x, x10, x100);
@@ -7568,20 +7573,24 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                     QUERY PLAN                      
------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Hash Join
    Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
    ->  Hash Join
          Hash Cond: (f3.x = f2.x)
          ->  Seq Scan on fkest f3
          ->  Hash
-               ->  Seq Scan on fkest f2
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f2
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Hash
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-(11 rows)
+         ->  Bitmap Heap Scan on fkest f1
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
+(15 rows)
 
 rollback;
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 3819bf5e2..58c2d013a 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5240,9 +5240,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index c73631a9a..62eb4239a 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1461,18 +1461,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index fe162cc7c..dea40e013 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -766,7 +766,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -776,7 +776,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 57de1acff..c0b621f40 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2662,6 +2663,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

In reply to: Peter Geoghegan (#40)
2 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Oct 18, 2024 at 12:17 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v11, which is yet another revision whose sole purpose is
to fix bit rot/make the patch apply cleanly against the master
branch's tip.

Attached is v12, which is yet another revision required only so that
the patch's latest version applies cleanly on top of master. (This
time it was the IWYU bulk header removals that caused the patch to bit
rot.)

--
Peter Geoghegan

Attachments:

v12-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/x-patch; name=v12-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From 395370c6e72d34dc0cd02a535dacf6e88becc873 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v12 1/2] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 doc/src/sgml/bloom.sgml                       |   6 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 21 files changed, 242 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index e1884acf4..7b4180db5 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -153,6 +153,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index c0b978119..52939d317 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -585,6 +585,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index f2fd62afb..5e423e155 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index b35b8a975..36f1435cb 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index 0d99d6abc..927ba1039 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 69c360843..3b62449d3 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 484ede8c2..8978ce411 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -69,6 +69,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -552,6 +553,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -578,6 +580,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -700,6 +703,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			*next_scan_page = btscan->btps_nextScanPage;
 			*last_curr_page = btscan->btps_lastCurrPage;
@@ -787,6 +795,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -821,6 +831,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index c33438c4b..9d22faf65 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -971,6 +971,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 301786185..be668abf2 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7c0fd63b2..6cb5ebcd2 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2098,6 +2100,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2111,6 +2114,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2127,6 +2131,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2645,6 +2650,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 19f2b172c..92b13f539 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -170,9 +170,10 @@ CREATE INDEX
    Heap Blocks: exact=28
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..1792.00 rows=2 width=0) (actual time=0.356..0.356 rows=29 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 0.408 ms
-(8 rows)
+(9 rows)
 </programlisting>
   </para>
 
@@ -202,11 +203,12 @@ CREATE INDEX
    -&gt;  BitmapAnd  (cost=24.34..24.34 rows=2 width=0) (actual time=0.027..0.027 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..12.04 rows=500 width=0) (actual time=0.026..0.026 rows=0 loops=1)
                Index Cond: (i5 = 123451)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
-(9 rows)
+(10 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 331315f8d..014a66ef7 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4180,12 +4180,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index ff689b652..1f2172960 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -702,8 +702,10 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Heap Blocks: exact=10
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 1
  Planning Time: 0.485 ms
  Execution Time: 0.073 ms
 </screen>
@@ -754,6 +756,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Heap Blocks: exact=90
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
  Planning Time: 0.187 ms
  Execution Time: 3.036 ms
 </screen>
@@ -819,6 +822,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -848,9 +852,11 @@ EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique
          Buffers: shared hit=4 read=3
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared hit=2
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
                Buffers: shared hit=2 read=3
  Planning:
    Buffers: shared hit=3
@@ -883,6 +889,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Heap Blocks: exact=90
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1017,6 +1024,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
  Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
  Planning Time: 0.077 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a854..e042638b7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -502,9 +502,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    Batches: 1  Memory Usage: 24kB
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(7 rows)
+(8 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index ae9ce9d8e..c24d56007 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index f6b8329cd..a8bc0bfd7 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7a03b4e36..e3e99272a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2657,47 +2661,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off)
@@ -2713,16 +2726,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2741,7 +2757,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off)
@@ -2757,16 +2773,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2787,7 +2806,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2877,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2897,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,17 +2986,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2982,17 +3013,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3064,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3048,17 +3091,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3112,17 +3161,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3144,17 +3199,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3482,12 +3543,14 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(15);
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3566,10 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(25);
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3617,17 @@ explain (analyze, costs off, summary off, timing off) select * from ma_test wher
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4197,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 33a6dceb0..b7cf35b9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index 2eaeb1477..9afe205e0 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 442428d93..085e746af 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

v12-0002-Add-skip-scan-to-nbtree.patchapplication/x-patch; name=v12-0002-Add-skip-scan-to-nbtree.patchDownload
From 206d122e80225b28a4c2811d79acbc7b534cbc45 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v12 2/2] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
useful in cases where the total number of distinct values in the column
'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.

The design of skip arrays allows array advancement to work without
concern for which specific array types are involved; only the lowest
level code needs to treat skip arrays and SAOP arrays differently.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values are conceptually just like any
other value that might appear in either kind of array.  A call made to
_bt_first to reposition the scan based on a NEXT/PRIOR sentinel usually
won't locate a leaf page containing tuples that must be returned to the
scan, but that's normal during any skip scan that happens to involve
"sparse" indexed values (skip support can only help with "dense" indexed
values, which isn't meaningfully possible when the skipped column is of
a continuous type, where skip support typically can't be implemented).

The optimizer doesn't use distinct new index paths to represent index
skip scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  Skipping is possible during
eligible bitmap index scans, index scans, and index-only scans.  It's
also possible during eligible parallel scans.  An eligible scan is any
scan that nbtree preprocessing generates a skip array for, which happens
whenever doing so will enable later preprocessing to mark original input
scan keys (passed by the executor) as required to continue the scan.

There are hardly any limitations around where skip arrays/scan keys may
appear relative to conventional/input scan keys.  This is no less true
in the presence of conventional SAOP array scan keys, which may both
roll over and be rolled over by skip arrays.  For example, a skip array
on the column "b" is generated for "WHERE a = 42 AND c IN (5, 6, 7, 8)".
As always (since commit 5bf748b8), whether or not nbtree actually skips
depends in large part on physical index characteristics at runtime.
Preprocessing is optimistic about skipping working out: it applies
simple static rules to decide where to place skip arrays.  It's up to
array-related scan code to keep the overhead of maintaining the scan's
skip arrays under control when it turns out that skipping isn't helpful.

Preprocessing will never cons up a skip array for an index column that
has an equality strategy scan key on input, but will do so for indexed
columns that are only constrained by inequality strategy scan keys.
This allows the scan to skip using a range skip array.  These are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   28 +-
 src/include/catalog/pg_amproc.dat             |   16 +
 src/include/catalog/pg_proc.dat               |   24 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  101 ++
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  273 +++
 src/backend/access/nbtree/nbtree.c            |  208 ++-
 src/backend/access/nbtree/nbtsearch.c         |   84 +-
 src/backend/access/nbtree/nbtutils.c          | 1466 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   46 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  379 +++--
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/uuid.c                  |   71 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/btree.sgml                       |   32 +
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   49 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |   10 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   61 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    5 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 34 files changed, 2706 insertions(+), 318 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c51de742e..0826c124f 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -202,7 +202,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 5fb523ece..e02332f59 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1022,10 +1024,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* skip scan support (unless !use_sksup) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo   *low_order;		/* low_compare's ORDER proc */
+	FmgrInfo   *high_order;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1114,6 +1128,10 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on omitted prefix column */
+#define SK_BT_NEGPOSINF	0x00080000	/* -inf/+inf key (invalid sk_argument) */
+#define SK_BT_NEXT		0x00100000	/* positions scan > sk_argument */
+#define SK_BT_PRIOR		0x00200000	/* positions scan < sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1150,6 +1168,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1160,7 +1182,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5d7fe292b..c0ccdfdfb 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -122,6 +128,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +149,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +170,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +205,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +275,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 1ec0d6f6b..fc1851ead 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2257,6 +2272,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4444,6 +4462,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -9325,6 +9346,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d70e6d37e..5b58739ad 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..d97e6059d
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,101 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining pairs of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code falls back on next-key sentinel values for any opclass that
+ * doesn't provide its own skip support function.  There is no benefit to
+ * providing skip support for an opclass on a type where guessing that the
+ * next indexed value is the next possible indexable value seldom works out.
+ * It might not even be feasible to add skip support for a continuous type.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order).
+	 * This gives the B-Tree code a useful value to start from, before any
+	 * data has been read from the index.  These are also sometimes used to
+	 * detect when the scan has run out of indexable values to search for.
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 *
+	 * Operator class decrement/increment functions never have to directly
+	 * deal with the value NULL, nor with ASC vs. DESC column ordering.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 1859be614..b4f1466d4 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..26ebbf776 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 8978ce411..b8129058b 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -29,6 +29,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -70,7 +71,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -78,11 +79,23 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -535,10 +548,156 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
 	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Assume every index attribute might require that we generate a skip scan
+	 * key
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		Form_pg_attribute attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array/skip scan key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array/skip scan key, while freeing memory allocated
+		 * for old sk_argument where required
+		 */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -549,7 +708,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -572,16 +732,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -610,6 +770,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false;
 	bool		status = true;
@@ -653,7 +814,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -668,14 +829,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				*next_scan_page = InvalidBlockNumber;
 				exit_loop = true;
 			}
@@ -713,7 +869,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -747,11 +903,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -788,7 +944,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -797,7 +953,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -815,6 +971,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -824,7 +981,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -834,14 +991,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 9d22faf65..67c7af2e2 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -883,7 +883,6 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	BTStack		stack;
 	OffsetNumber offnum;
-	StrategyNumber strat;
 	BTScanInsertData inskey;
 	ScanKey		startKeys[INDEX_MAX_KEYS];
 	ScanKeyData notnullkeys[INDEX_MAX_KEYS];
@@ -983,7 +982,17 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * a > or < boundary or find an attribute with no boundary (which can be
 	 * thought of as the same as "> -infinity"), we can't use keys for any
 	 * attributes to its right, because it would break our simplistic notion
-	 * of what initial positioning strategy to use.
+	 * of what initial positioning strategy to use.  In practice skip scan
+	 * typically enables us to use all scan keys here, even with a set of
+	 * input keys that leave a "gap" between two index attributes (cases with
+	 * multiple gaps will even manage this without any special restrictions).
+	 *
+	 * Skip scan works by having _bt_preprocess_keys cons up = boundary keys
+	 * for any index columns that were missing a = key in scan->keyData[], the
+	 * input scan keys passed to us by the executor.  This happens for index
+	 * attributes prior to the attribute of our final input scan key.  The
+	 * underlying = keys use skip arrays.  A skip array key on the column x
+	 * can be thought of as "x = ANY('{every possible x value}')".
 	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
@@ -1058,6 +1067,35 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 		{
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					/* -inf/+inf element from a skip array's scan key */
+					ScanKey		skiparraysk = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == skiparraysk - so->keyData)
+							break;
+					}
+
+					/* use array's inequality key in startKeys[] */
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					/*
+					 * If the skip array doesn't include a NULL element, it
+					 * implies a NOT NULL constraint
+					 */
+					if (!array->null_elem)
+						impliesNN = skiparraysk;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
 				/*
 				 * Done looking at keys for curattr.  If we didn't find a
 				 * usable boundary key, see if we can deduce a NOT NULL key.
@@ -1091,16 +1129,48 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 				startKeys[keysz++] = chosen;
 
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					/*
+					 * Next/prior key element from a skip array's scan key.
+					 * Adjust strat_total, so that our = key gets treated like
+					 * a > key (or like a < key) within _bt_search.
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).
+					 */
+					Assert(strat_total == BTEqualStrategyNumber);
+					Assert(!(chosen->sk_flags & SK_SEARCHNOTNULL));
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[]
+					 */
+					break;
+				}
+
 				/*
 				 * Adjust strat_total, and quit if we have stored a > or <
 				 * key.
 				 */
-				strat = chosen->sk_strategy;
-				if (strat != BTEqualStrategyNumber)
+				if (chosen->sk_strategy != BTEqualStrategyNumber)
 				{
-					strat_total = strat;
-					if (strat == BTGreaterStrategyNumber ||
-						strat == BTLessStrategyNumber)
+					strat_total = chosen->sk_strategy;
+					if (chosen->sk_strategy == BTGreaterStrategyNumber ||
+						chosen->sk_strategy == BTLessStrategyNumber)
 						break;
 				}
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 76be65123..11f8fc4dc 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	Oid			eq_op;			/* = op to be used to add array, if any */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -64,15 +92,37 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno,
+							BTSkipPreproc *skipatts);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -256,6 +306,12 @@ _bt_freestack(BTStack stack)
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -271,10 +327,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numArrayKeyData,
+				numSkipArrayKeys;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -282,11 +340,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
+	Assert(scan->numberOfKeys);
 
-	/* Quick check to see if there are any array keys */
+	/*
+	 * Quick check to see if there are any array keys, or any missing keys we
+	 * can generate a "skip scan" array key for ourselves
+	 */
 	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
+	numArrayKeyData = scan->numberOfKeys;	/* Initial arrayKeyData[] size */
+	for (int i = 0; i < scan->numberOfKeys; i++)
 	{
 		cur = &scan->keyData[i];
 		if (cur->sk_flags & SK_SEARCHARRAY)
@@ -302,6 +364,15 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		}
 	}
 
+	numSkipArrayKeys = _bt_decide_skipatts(scan, skipatts);
+	if (numSkipArrayKeys)
+	{
+		/* At least one skip array must be added to so->arrayKeys[] */
+		numArrayKeys += numSkipArrayKeys;
+		/* Associated scan keys must be added to arrayKeyData[], too */
+		numArrayKeyData += numSkipArrayKeys;
+	}
+
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
@@ -320,17 +391,23 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/*
+	 * Process each array key, and generate skip arrays as needed.  Also copy
+	 * every scan->keyData[] input scan key (whether it's an array or not)
+	 * into the arrayKeyData array we'll return to our caller (barring any
+	 * array scan keys that we could eliminate early through array merging).
+	 */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -346,16 +423,82 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and scan key where indicated by skipatts */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* won't skip using this attribute */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[numArrayKeyData];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].low_order = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].high_order = NULL;	/* for now */
+
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how the
+			 * consed-up "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			numArrayKeyData++;	/* keep this scan key/array */
+		}
+
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
+		cur = &arrayKeyData[numArrayKeyData];
 		*cur = scan->keyData[input_ikey];
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
@@ -414,7 +557,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -425,7 +568,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -442,7 +585,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -517,17 +660,23 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].low_order = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].high_order = NULL;	/* unused */
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -633,7 +782,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -684,7 +834,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -694,6 +844,191 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_decide_skipatts() -- set index attributes requiring skip arrays
+ *
+ * _bt_preprocess_array_keys helper function.  Determines which attributes
+ * will require skip arrays/scan keys.  Also sets up skip support callbacks
+ * for attributes whose input opclass have skip support (opclasses without
+ * skip support will fall back on using next-key sentinel values when
+ * advancing the skip array to its next array element).
+ *
+ * Return value is the total number of scan keys to add as "input" scan keys
+ * for further processing within _bt_preprocess_keys.
+ */
+static int
+_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_inkey = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+		int			prev_numSkipArrayKeys = numSkipArrayKeys;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				return prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			numSkipArrayKeys++;
+			attno_skip++;
+		}
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				numSkipArrayKeys++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				return numSkipArrayKeys;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	return numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatts
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatts)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatts->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										  BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack even an equality strategy
+	 * operator, but they're supported.  We cope by making caller not use a
+	 * skip array for this index column (nor for any lower-order columns).
+	 */
+	if (!OidIsValid(skipatts->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatts->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+														reverse,
+														&skipatts->sksup);
+
+	/* might not have set up skip support, but can skip either way */
+	return true;
+}
+
 /*
  * _bt_setup_array_cmp() -- Set up array comparison functions
  *
@@ -980,26 +1315,28 @@ _bt_merge_arrays(IndexScanDesc scan, ScanKey skey, FmgrInfo *sortproc,
  * guaranteeing that at least the scalar scan key can be considered redundant.
  * We return false if the comparison could not be made (caller must keep both
  * scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar inequality scan keys.
  */
 static bool
 _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
 	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
 		   skey->sk_strategy != BTEqualStrategyNumber);
@@ -1009,8 +1346,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.  We have to be sure
+	 * that _bt_compare_array_skey/_bt_binsrch_array_skey use the right proc.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1041,11 +1378,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensures that the scalar scan key can be eliminated as redundant
+		 */
+		eliminated = true;
+
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+	}
+	else
+	{
+		/*
+		 * With a skip array, it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when there's a lack of suitable cross-type support for
+		 * comparing skey to an existing inequality associated with the array.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when it
+ * is redundant with (or contradicted by) a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1097,6 +1488,123 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of skip array scan key when it is "redundant with"
+ * a non-array scalar scan key.  The scalar scan key must be an inequality.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array, forcing its elements to be generated within the limits of a range.
+ *
+ * Return value indicates if the scalar scan key could be "eliminated".  We
+ * return true in the common case where caller's scan key was successfully
+ * rolled into the skip array.  We return false when we can't do that due to
+ * the presence of an existing, conflicting inequality that cannot be compared
+ * to caller's inequality due to a lack of suitable cross-type support.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way scan code can assume that it'll always
+	 * be safe to use the same-type ORDER procs stored in so->orderProcs[],
+	 * provided the array's scan key isn't marked NEGPOSINF.  When the array's
+	 * scan key is marked NEGPOSINF, then it'll lack a valid sk_argument, and
+	 * it'll be necessary to apply low_compare and/or high_compare separately.
+	 *
+	 * Under this scheme, there isn't any danger of anybody becoming confused
+	 * about which type is used where, even in cross-type scenarios; each scan
+	 * key consistently uses the same underlying type.  While NEGPOSINF is a
+	 * distinct array element, it is only used to force low_compare and/or
+	 * high_compare to find the real first (or final) element in the index.
+	 * You can think of NEGPOSINF as representing an imaginary point in the
+	 * key space immediately before the start (or immediately after the end)
+	 * of the true first (or true final) matching value.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (!array->high_compare)
+			{
+				/* array currently lacks a high_compare, make space for one */
+				array->high_compare = palloc(sizeof(ScanKeyData));
+				array->high_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare, keep old one */
+
+				/* replace old high_compare with caller's new one */
+			}
+
+			/* caller's high_compare becomes skip array's high_compare */
+			*array->high_compare = *skey;
+			*array->high_order = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (!array->low_compare)
+			{
+				/* array currently lacks a low_compare, make space for one */
+				array->low_compare = palloc(sizeof(ScanKeyData));
+				array->low_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare, keep old one */
+
+				/* replace old low_compare with caller's new one */
+			}
+
+			/* caller's low_compare becomes skip array's low_compare */
+			*array->low_compare = *skey;
+			*array->low_order = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
@@ -1220,6 +1728,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1342,6 +1852,98 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, because the array doesn't really
+ * have any elements (it generates its array elements procedurally instead).
+ * Note that this may include a NULL value/an IS NULL qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ *
+ * Note: This function doesn't actually binary search.  It uses an interface
+ * that's as similar to _bt_binsrch_array_skey as possible for consistency.
+ * "Binary searching a skip array" just means determining if tupdatum/tupnull
+ * are before, within, or beyond the range of caller's skip array.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1351,29 +1953,482 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting skip array to lowest element, which isn't just NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Setting skip array to highest element, which isn't just NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		underflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element is out of bounds for the array.
+		 *
+		 * This is only possible if low_compare represents an >= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the prior value cannot possibly satisfy low_compare, so we can
+		 * give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(array->low_order,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Decrement by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &underflow);
+
+	if (underflow)
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the bounds of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		overflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element is out of bounds for the array.
+		 *
+		 * This is only possible if high_compare represents an <= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the next value cannot possibly satisfy high_compare, so we can
+		 * give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(array->high_order,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Increment by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &overflow);
+
+	if (overflow)
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the bounds of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1389,6 +2444,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1398,29 +2454,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1475,6 +2532,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1482,7 +2540,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1494,16 +2551,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1628,9 +2679,94 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_NEGPOSINF))
+		{
+			/* Scankey has a valid/comparable sk_argument value (not -inf) */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXT))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument + infinitesimal"
+				 */
+				if (ScanDirectionIsForward(dir))
+				{
+					/* tupdatum is still < "sk_argument + infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was forward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to backward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+			else if (result == 0 && (cur->sk_flags & SK_BT_PRIOR))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument - infinitesimal"
+				 */
+				if (ScanDirectionIsBackward(dir))
+				{
+					/* tupdatum is still > "sk_argument - infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was backward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to forward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+		}
+		else
+		{
+			/*
+			 * Scankey searches for the sentinel value -inf (or +inf).
+			 *
+			 * Note: -inf could mean "absolute" -inf, or it could represent an
+			 * imaginary point in the key space that comes immediately before
+			 * the first real value that satisfies the array's low_compare.
+			 * +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * Compare tupdatum against -inf using array's low_compare, if any
+			 * (or compare it against +inf using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is > -inf sk_argument (or < +inf sk_argument).
+				 * It's time for caller to advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1962,18 +3098,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1998,18 +3125,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2027,12 +3145,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -2108,11 +3235,62 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  This is often the range -inf through to +inf.
+		 * "Binary searching" only determined whether tupdatum is beyond,
+		 * before, or within the range of the skip array.
+		 *
+		 * As always, we set the array element to its closest available match.
+		 * But unlike with a conventional array, a skip array's new element
+		 * might be NEGPOSINF, a special sentinel value.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == "some array element".
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2458,6 +3636,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also cons up skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2470,10 +3650,16 @@ end_toplevel_scan:
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never consed up skip array scan keys, it would be possible for "gaps"
+ * to appear that made it unsafe to mark any subsequent input scan keys (taken
+ * from scan->keyData[]) as required to continue the scan.  If there are no
+ * keys for a given attribute, the keys for subsequent attributes can never be
+ * required.  For instance, "WHERE y = 4" always required a full-index scan on
+ * version prior to Postgres 18.  Now preprocessing rewrites such a qual into
+ * "WHERE x = ANY('{every possible x value}') and y = 4", thereby enabling
+ * marking the key on y (and the key on x) required to continue the scan.
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -2510,7 +3696,12 @@ end_toplevel_scan:
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That won't work with skip arrays, which work by making a value into the
+ * current array element, which anchors later scan keys via "=" comparisons.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -2574,6 +3765,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -2713,7 +3912,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
+						Assert(!array || array->num_elems > 0 ||
+							   array->num_elems == -1);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -2761,6 +3961,16 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * Our call to _bt_preprocess_array_keys is generally expected to
+			 * have already added "=" scan keys with skip arrays as required
+			 * to form an unbroken series of "=" constraints on all attrs
+			 * prior to the attr from the last scan key that came from our
+			 * original set of scan->keyData[] input scan keys.  Despite all
+			 * this, we cannot assume _bt_preprocess_array_keys will always
+			 * make it safe for us to mark all scan keys required.  Certain
+			 * corner cases might have prevented it from adding a skip array
+			 * (e.g., opclasses without an "=" operator can't use "=" arrays).
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -2849,6 +4059,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -2857,6 +4068,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -2876,7 +4088,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
+					Assert(!array || array->num_elems > 0 ||
+						   array->num_elems == -1);
 
 					/*
 					 * New key is more restrictive, and so replaces old key...
@@ -3018,10 +4231,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3087,6 +4301,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -3096,6 +4313,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3169,6 +4402,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3730,6 +4964,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index db6ed784a..86a5de8f4 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 8efb4044d..6efa3e353 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -372,6 +372,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 85e5eaf32..88c929a0a 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -98,6 +98,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 8130f3e8a..256350007 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f73f294b8..cb8470324 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -85,6 +85,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 08fa6774d..42fde7ef2 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -192,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -213,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5736,6 +5740,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6794,6 +6884,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6803,17 +6940,22 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6829,14 +6971,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -6846,13 +6993,90 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't absord a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6874,6 +7098,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6894,7 +7119,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6910,6 +7135,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6925,6 +7182,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -7033,104 +7291,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 5284d23dc..654bda638 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -384,6 +387,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 8a67f0120..6befee3a4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1751,6 +1752,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3590,6 +3602,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..d256e091f 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index d3358dfc3..fa6f27e6d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1938,7 +1938,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -1958,7 +1958,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 9b2973694..4c4909691 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4440,24 +4440,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -7562,19 +7563,23 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                        QUERY PLAN                         
------------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Nested Loop
    ->  Hash Join
          Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-         ->  Seq Scan on fkest f2
-               Filter: (x100 = 2)
+         ->  Bitmap Heap Scan on fkest f2
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
          ->  Hash
-               ->  Seq Scan on fkest f1
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f1
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
          Index Cond: (x = f1.x)
-(10 rows)
+(14 rows)
 
 alter table fkest add constraint fk
   foreign key (x, x10b, x100) references fkest (x, x10, x100);
@@ -7583,20 +7588,24 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                     QUERY PLAN                      
------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Hash Join
    Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
    ->  Hash Join
          Hash Cond: (f3.x = f2.x)
          ->  Seq Scan on fkest f3
          ->  Hash
-               ->  Seq Scan on fkest f2
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f2
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Hash
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-(11 rows)
+         ->  Bitmap Heap Scan on fkest f1
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
+(15 rows)
 
 rollback;
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 3819bf5e2..58c2d013a 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5240,9 +5240,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index c73631a9a..62eb4239a 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1461,18 +1461,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index fe162cc7c..dea40e013 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -766,7 +766,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -776,7 +776,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 171a7dd5d..b4c2559fb 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2664,6 +2665,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

In reply to: Peter Geoghegan (#41)
2 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Mon, Oct 28, 2024 at 12:38 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v12, which is yet another revision required only so that
the patch's latest version applies cleanly on top of master.

Attached is v13, which is the first revision posted that has new
functional changes in quite some time (this isn't just for fixing
bitrot).

The notable change for v13 is the addition of preprocessing that
adjusts skip array attributes > and < operators. These are used for
initial position purposes by _bt_first -- the purpose of this new
preprocessing/transformation is to minimize the number of descents of
the index in certain corner cases involving > and < operators, by
converting them into similar >= and <= operators during preprocessing
where possible. This transformation relies on the opclass skip support
routine, and is strictly optional.

Imagine a qual like "WHERE a > 5" AND b = 3". This will use a range
style skip array on "a", and a regular required scan key on "b". The
new transformation makes the final scan keys output by preprocessing
match what you'd get if the qual had been written "WHERE a >= 6 AND b
= 3". This leads to a modest reduction in the number of descents
required in some cases. It's possible that a naive "a > 5" initial
descent within _bt_first would have wastefully landed on an earlier
leaf page. Whereas now, with v13, our more discriminating "a >= 6 AND
b = 3" initial _bt_first descent will manage to land directly on the
true first page (the inclusion of "b = 3" in the first _bt_first call
makes this possible).

Obviously, the new transformation won't help most individual calls to
_bt_first when a skip array (with a low_compare inequality) is used,
since most individual calls will already have a real skip array
element value (not just the sentinel value -inf or +inf) to work off
of. This is not an essential optimization (it only optimizes the first
_bt_first call for the whole skip scan), but it seems worth having.
Very early versions of the patch had this same optimization, though
that was implemented in a way that wasn't compatible with cross-type
operators. The approach taken in v13 has no such problems, and does
things in a way that's very well targeted -- there's no impact on any
of the other preprocessing steps.

--
Peter Geoghegan

Attachments:

v13-0002-Add-skip-scan-to-nbtree.patchapplication/x-patch; name=v13-0002-Add-skip-scan-to-nbtree.patchDownload
From 54d7c466caaf8e15ae60e4cde22c2f33de9f3bb0 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v13 2/2] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
useful in cases where the total number of distinct values in the column
'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.

The design of skip arrays allows array advancement to work without
concern for which specific array types are involved; only the lowest
level code needs to treat skip arrays and SAOP arrays differently.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values are conceptually just like any
other value that might appear in either kind of array.  A call made to
_bt_first to reposition the scan based on a NEXT/PRIOR sentinel usually
won't locate a leaf page containing tuples that must be returned to the
scan, but that's normal during any skip scan that happens to involve
"sparse" indexed values (skip support can only help with "dense" indexed
values, which isn't meaningfully possible when the skipped column is of
a continuous type, where skip support typically can't be implemented).

The optimizer doesn't use distinct new index paths to represent index
skip scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  Skipping is possible during
eligible bitmap index scans, index scans, and index-only scans.  It's
also possible during eligible parallel scans.  An eligible scan is any
scan that nbtree preprocessing generates a skip array for, which happens
whenever doing so will enable later preprocessing to mark original input
scan keys (passed by the executor) as required to continue the scan.

There are hardly any limitations around where skip arrays/scan keys may
appear relative to conventional/input scan keys.  This is no less true
in the presence of conventional SAOP array scan keys, which may both
roll over and be rolled over by skip arrays.  For example, a skip array
on the column "b" is generated for "WHERE a = 42 AND c IN (5, 6, 7, 8)".
As always (since commit 5bf748b8), whether or not nbtree actually skips
depends in large part on physical index characteristics at runtime.
Preprocessing is optimistic about skipping working out: it applies
simple static rules to decide where to place skip arrays.  It's up to
array-related scan code to keep the overhead of maintaining the scan's
skip arrays under control when it turns out that skipping isn't helpful.

Preprocessing will never cons up a skip array for an index column that
has an equality strategy scan key on input, but will do so for indexed
columns that are only constrained by inequality strategy scan keys.
This allows the scan to skip using a range skip array.  These are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   28 +-
 src/include/catalog/pg_amproc.dat             |   16 +
 src/include/catalog/pg_proc.dat               |   24 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  101 +
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  273 +++
 src/backend/access/nbtree/nbtree.c            |  208 +-
 src/backend/access/nbtree/nbtsearch.c         |   74 +-
 src/backend/access/nbtree/nbtutils.c          | 1697 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   46 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  379 +++-
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/uuid.c                  |   71 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/btree.sgml                       |   34 +-
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   49 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |   10 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   61 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    5 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 34 files changed, 2915 insertions(+), 332 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c51de742e..0826c124f 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -202,7 +202,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 123fba624..d841e85bc 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1022,10 +1024,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* skip scan support (unless !use_sksup) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo   *low_order;		/* low_compare's ORDER proc */
+	FmgrInfo   *high_order;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1113,6 +1127,10 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on omitted prefix column */
+#define SK_BT_NEGPOSINF	0x00080000	/* -inf/+inf key (invalid sk_argument) */
+#define SK_BT_NEXT		0x00100000	/* positions scan > sk_argument */
+#define SK_BT_PRIOR		0x00200000	/* positions scan < sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1149,6 +1167,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1159,7 +1181,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5d7fe292b..c0ccdfdfb 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -122,6 +128,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +149,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +170,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +205,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +275,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index a38e20f5d..f7dd565ef 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2260,6 +2275,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4447,6 +4465,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -9328,6 +9349,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d70e6d37e..5b58739ad 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..d97e6059d
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,101 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining pairs of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code falls back on next-key sentinel values for any opclass that
+ * doesn't provide its own skip support function.  There is no benefit to
+ * providing skip support for an opclass on a type where guessing that the
+ * next indexed value is the next possible indexable value seldom works out.
+ * It might not even be feasible to add skip support for a continuous type.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order).
+	 * This gives the B-Tree code a useful value to start from, before any
+	 * data has been read from the index.  These are also sometimes used to
+	 * detect when the scan has run out of indexable values to search for.
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 *
+	 * Operator class decrement/increment functions never have to directly
+	 * deal with the value NULL, nor with ASC vs. DESC column ordering.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 1859be614..b4f1466d4 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..26ebbf776 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 2933b5830..77953a9ab 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -29,6 +29,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -70,7 +71,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -78,11 +79,23 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -535,10 +548,156 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
 	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Assume every index attribute might require that we generate a skip scan
+	 * key
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		Form_pg_attribute attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array/skip scan key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array/skip scan key, while freeing memory allocated
+		 * for old sk_argument where required
+		 */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -549,7 +708,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -572,16 +732,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -610,6 +770,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false;
 	bool		status = true;
@@ -653,7 +814,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -668,14 +829,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				*next_scan_page = InvalidBlockNumber;
 				exit_loop = true;
 			}
@@ -713,7 +869,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -747,11 +903,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -790,7 +946,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -799,7 +955,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -817,6 +973,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -826,7 +983,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -836,14 +993,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 3eebc04be..d6168ba59 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -984,6 +984,13 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  Skip
+	 * array keys are = keys, though we'll sometimes need to treat the key as
+	 * if it was some kind of inequality instead.  This is required whenever
+	 * the skip array's current array element is a sentinel value.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1058,8 +1065,38 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is -inf or +inf, choose low_compare/high_compare
+				 * (or consider if they imply a NOT NULL constraint).
+				 */
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					ScanKey		skiparraysk = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int j = 0; j < so->numArrayKeys; j++)
+					{
+						array = &so->arrayKeys[j];
+						if (array->scan_key == skiparraysk - so->keyData)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					if (!array->null_elem)
+						impliesNN = skiparraysk;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1096,6 +1133,39 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					strat_total == BTLessStrategyNumber)
 					break;
 
+				/*
+				 * If this is a scan key for a skip array whose current
+				 * element is marked NEXT or PRIOR, adjust strat_total
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(strat_total == BTEqualStrategyNumber);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[].
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).
+					 */
+					break;
+				}
+
 				/*
 				 * Done if that was the last attribute, or if next key is not
 				 * in sequence (implying no boundary key is available for the
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 1b75066fb..92bf228e6 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	Oid			eq_op;			/* = op to be used to add array, if any */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -63,16 +91,46 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
+static int	_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+										  int *numSkipArrayKeys);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_decrement(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_increment(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -256,6 +314,12 @@ _bt_freestack(BTStack stack)
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -271,10 +335,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -282,30 +348,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_preprocess_num_array_keys(scan, skipatts,
+												 &numSkipArrayKeys);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys described within skipatts[]
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -320,17 +380,18 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/* Process input keys, and emit skip array keys described by skipatts[] */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -346,19 +407,97 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and its scan key where indicated */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* won't skip using this attribute */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[numArrayKeyData];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].low_order = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].high_order = NULL;	/* for now */
+
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how the
+			 * consed-up "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			numArrayKeyData++;	/* keep this scan key/array */
+		}
+
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
+		cur = &arrayKeyData[numArrayKeyData];
 		*cur = scan->keyData[input_ikey];
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -414,7 +553,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -425,7 +564,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -442,7 +581,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -517,23 +656,232 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].low_order = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].high_order = NULL;	/* unused */
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
 	return arrayKeyData;
 }
 
+/*
+ *	_bt_preprocess_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit all required so->arrayKeys[] entries.
+ *
+ * Also sets *numSkipArrayKeys to the number of skip arrays that caller must
+ * make sure to add to the scan keys it'll output (complete with corresponding
+ * so->arrayKeys[] entries), and sets the corresponding attributes positions
+ * in *skipatts argument.  Every attribute that receives a skip array will
+ * have its skip support routine set in *skipatts, too.
+ */
+static int
+_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+							  int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_inkey = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+		int			prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				(*numSkipArrayKeys)++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				break;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatt
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatt->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										 BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack even an equality strategy
+	 * operator, but they're supported.  We cope by making caller not use a
+	 * skip array for this index column (nor for any lower-order columns).
+	 */
+	if (!OidIsValid(skipatt->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatt->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+													   reverse,
+													   &skipatt->sksup);
+
+	/* might not have set up skip support, but can skip either way */
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys_final() -- fix up array scan key references
  *
@@ -633,7 +981,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -669,6 +1018,14 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && !array->null_elem)
+						_bt_skip_preproc_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
@@ -684,7 +1041,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -980,26 +1337,28 @@ _bt_merge_arrays(IndexScanDesc scan, ScanKey skey, FmgrInfo *sortproc,
  * guaranteeing that at least the scalar scan key can be considered redundant.
  * We return false if the comparison could not be made (caller must keep both
  * scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar inequality scan keys.
  */
 static bool
 _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
 	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
 		   skey->sk_strategy != BTEqualStrategyNumber);
@@ -1009,8 +1368,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.  We have to be sure
+	 * that _bt_compare_array_skey/_bt_binsrch_array_skey use the right proc.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1041,11 +1400,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensures that the scalar scan key can be eliminated as redundant
+		 */
+		eliminated = true;
+
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+	}
+	else
+	{
+		/*
+		 * With a skip array, it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when there's a lack of suitable cross-type support for
+		 * comparing skey to an existing inequality associated with the array.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when
+ * determining its redundancy against a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1097,10 +1510,295 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of a skip array scan key when determining its
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array, forcing its elements to be generated within the limits of a range.
+ *
+ * Return value indicates if the scalar scan key could be "eliminated".  We
+ * return true in the common case where caller's scan key was successfully
+ * rolled into the skip array.  We return false when we can't do that due to
+ * the presence of an existing, conflicting inequality that cannot be compared
+ * to caller's inequality due to a lack of suitable cross-type support.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way scan code can assume that it'll always
+	 * be safe to use the same-type ORDER procs stored in so->orderProcs[],
+	 * provided the array's scan key isn't marked NEGPOSINF.  When the array's
+	 * scan key is marked NEGPOSINF, then it'll lack a valid sk_argument, and
+	 * it'll be necessary to apply low_compare and/or high_compare separately.
+	 *
+	 * Under this scheme, there isn't any danger of anybody becoming confused
+	 * about which type is used where, even in cross-type scenarios; each scan
+	 * key consistently uses the same underlying type.  While NEGPOSINF is a
+	 * distinct array element, it is only used to force low_compare and/or
+	 * high_compare to find the real first (or final) element in the index.
+	 * You can think of NEGPOSINF as representing an imaginary point in the
+	 * key space immediately before the start (or immediately after the end)
+	 * of the true first (or true final) matching value.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (!array->high_compare)
+			{
+				/* array currently lacks a high_compare, make space for one */
+				array->high_compare = palloc(sizeof(ScanKeyData));
+				array->high_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare, keep old one */
+
+				/* replace old high_compare with caller's new one */
+			}
+
+			/* caller's high_compare becomes skip array's high_compare */
+			*array->high_compare = *skey;
+			*array->high_order = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (!array->low_compare)
+			{
+				/* array currently lacks a low_compare, make space for one */
+				array->low_compare = palloc(sizeof(ScanKeyData));
+				array->low_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare, keep old one */
+
+				/* replace old low_compare with caller's new one */
+			}
+
+			/* caller's low_compare becomes skip array's low_compare */
+			*array->low_compare = *skey;
+			*array->low_order = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
 
+/*
+ * Perform final preprocessing for a skip array.  Called when the skip array's
+ * final low_compare and high_compare have been chosen.
+ *
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts the
+ * operator strategies).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value NEGPOSINF, using >= instead of using >
+ * (or using <= instead of < during backwards scans) makes it safe to include
+ * lower-order scan keys in the insertion scan key (there must be lower-order
+ * scan keys after the skip array).  We will avoid an extra _bt_first to find
+ * the first value in the index > sk_argument, at least when the first real
+ * matching value in the index happens to be an exact match for the newly
+ * transformed (newly incremented/decremented) sk_argument value.
+ */
+static void
+_bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+	Assert(!array->null_elem);
+
+	/* If skip array doesn't have skip support, can't apply optimization */
+	if (!array->use_sksup)
+		return;
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skip_preproc_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skip_preproc_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skip_preproc_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	bool		reverse = (arraysk->sk_flags & SK_BT_DESC) != 0;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
+	Oid			sktype = opcintype;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	SkipSupport ss = &array->sksup;
+	SkipSupportData sksup;
+	Oid			cmp_op;
+	RegProcedure cmp_proc;
+	bool		underflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+	{
+		/* hard case: have to look up separate skip support routine */
+		ss = &sksup;
+		sktype = high_compare->sk_subtype;
+		if (!PrepareSkipSupportFromOpclass(opfamily, sktype, reverse, ss))
+			return;
+
+		/* successfully looked up other opclass' skip support routine */
+	}
+
+	/* Look up relevant <= operator (might fail) */
+	cmp_op = get_opfamily_member(opfamily, opcintype, sktype,
+								 BTLessEqualStrategyNumber);
+	if (!OidIsValid(cmp_op))
+		return;
+	new_sk_argument = ss->decrement(rel, orig_sk_argument, &underflow);
+
+	/*
+	 * It might be possible to handle underflow by marking the whole qual
+	 * unsatisfiable, but we just back out of the optimization instead.
+	 * That keeps things simpler in cross-type scenarios.
+	 */
+	if (underflow)
+		return;
+
+	cmp_proc = get_opcode(cmp_op);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skip_preproc_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	bool		reverse = (arraysk->sk_flags & SK_BT_DESC) != 0;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
+	Oid			sktype = opcintype;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	SkipSupport ss = &array->sksup;
+	SkipSupportData sksup;
+	Oid			cmp_op;
+	RegProcedure cmp_proc;
+	bool		overflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+	{
+		/* hard case: have to look up separate skip support routine */
+		ss = &sksup;
+		sktype = low_compare->sk_subtype;
+		if (!PrepareSkipSupportFromOpclass(opfamily, sktype, reverse, ss))
+			return;
+
+		/* successfully looked up other opclass' skip support routine */
+	}
+
+	/* Look up relevant >= operator (might fail) */
+	cmp_op = get_opfamily_member(opfamily, opcintype, sktype,
+								 BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(cmp_op))
+		return;
+	new_sk_argument = ss->increment(rel, orig_sk_argument, &overflow);
+
+	/*
+	 * It might be possible to handle overflow by marking the whole qual
+	 * unsatisfiable, but we just back out of the optimization instead.
+	 * That keeps things simpler in cross-type scenarios.
+	 */
+	if (overflow)
+		return;
+
+	cmp_proc = get_opcode(cmp_op);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  * qsort_arg comparator for sorting array elements
  */
@@ -1220,6 +1918,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1342,6 +2042,98 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, because the array doesn't really
+ * have any elements (it generates its array elements procedurally instead).
+ * Note that this may include a NULL value/an IS NULL qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ *
+ * Note: This function doesn't actually binary search.  It uses an interface
+ * that's as similar to _bt_binsrch_array_skey as possible for consistency.
+ * "Binary searching a skip array" just means determining if tupdatum/tupnull
+ * are before, within, or beyond the range of caller's skip array.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1351,29 +2143,482 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting skip array to lowest element, which isn't just NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Setting skip array to highest element, which isn't just NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		underflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element is out of bounds for the array.
+		 *
+		 * This is only possible if low_compare represents an >= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the prior value cannot possibly satisfy low_compare, so we can
+		 * give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(array->low_order,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Decrement by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &underflow);
+
+	if (underflow)
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the bounds of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		overflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element is out of bounds for the array.
+		 *
+		 * This is only possible if high_compare represents an <= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the next value cannot possibly satisfy high_compare, so we can
+		 * give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(array->high_order,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Increment by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &overflow);
+
+	if (overflow)
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the bounds of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1389,6 +2634,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1398,29 +2644,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1480,6 +2727,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1487,7 +2735,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1499,16 +2746,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1633,9 +2874,94 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_NEGPOSINF))
+		{
+			/* Scankey has a valid/comparable sk_argument value (not -inf) */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXT))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument + infinitesimal"
+				 */
+				if (ScanDirectionIsForward(dir))
+				{
+					/* tupdatum is still < "sk_argument + infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was forward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to backward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+			else if (result == 0 && (cur->sk_flags & SK_BT_PRIOR))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument - infinitesimal"
+				 */
+				if (ScanDirectionIsBackward(dir))
+				{
+					/* tupdatum is still > "sk_argument - infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was backward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to forward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+		}
+		else
+		{
+			/*
+			 * Scankey searches for the sentinel value -inf (or +inf).
+			 *
+			 * Note: -inf could mean "absolute" -inf, or it could represent an
+			 * imaginary point in the key space that comes immediately before
+			 * the first real value that satisfies the array's low_compare.
+			 * +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * Compare tupdatum against -inf using array's low_compare, if any
+			 * (or compare it against +inf using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is > -inf sk_argument (or < +inf sk_argument).
+				 * It's time for caller to advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1967,18 +3293,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -2003,18 +3320,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2032,12 +3340,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -2113,11 +3430,62 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  This is often the range -inf through to +inf.
+		 * "Binary searching" only determined whether tupdatum is beyond,
+		 * before, or within the range of the skip array.
+		 *
+		 * As always, we set the array element to its closest available match.
+		 * But unlike with a conventional array, a skip array's new element
+		 * might be NEGPOSINF, a special sentinel value.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == "some array element".
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2465,6 +3833,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also cons up skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2477,10 +3847,24 @@ end_toplevel_scan:
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never consed up skip array scan keys, it would be possible for "gaps"
+ * to appear that made it unsafe to mark any subsequent input scan keys (taken
+ * from scan->keyData[]) as required to continue the scan.  If there are no
+ * keys for a given attribute, the keys for subsequent attributes can never be
+ * required.  For instance, "WHERE y = 4" always required a full-index scan
+ * prior to Postgres 18.  Typically, preprocessing is now able to "rewrite"
+ * such a qual into "WHERE x = ANY('{every possible x value}') and y = 4".
+ * The addition of a consed up skip array key on "x" enables marking the key
+ * on "y" (as well as the one on "x") required, according to the usual rules.
+ * Of course, this transformation process cannot change the tuples returned by
+ * the scan -- the skip array will use an IS NULL qual when searching for its
+ * "NULL element" (in this particular example, at least).  But skip arrays can
+ * make the scan much more efficient: if there are few distinct values in "x",
+ * we'll be able to skip over many irrelevant leaf pages.  (If on the other
+ * hand there are many distinct values in "x" then the scan will degenerate
+ * into a full index scan at run time, but we'll be no worse off overall.)
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -2517,7 +3901,12 @@ end_toplevel_scan:
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That won't work with skip arrays, which work by making a value into the
+ * current array element, which anchors later scan keys via "=" comparisons.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -2581,6 +3970,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -2667,7 +4064,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			/*
 			 * If = has been specified, all other keys can be eliminated as
 			 * redundant.  If we have a case like key = 1 AND key > 2, we can
-			 * set qual_ok to false and abandon further processing.
+			 * set qual_ok to false and abandon further processing.  Note that
+			 * this is no less true if the = key is SEARCHARRAY; the only real
+			 * difference is that the inequality key _becomes_ redundant by
+			 * making _bt_compare_scankey_args eliminate a subset of array
+			 * elements (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * We also have to deal with the case of "key IS NULL", which is
 			 * unsatisfiable in combination with any other index condition. By
@@ -2720,7 +4121,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -2768,6 +4168,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * Typically, our call to _bt_preprocess_array_keys will have
+			 * already added "=" skip array keys as required to form an
+			 * unbroken series of "=" constraints on all attrs prior to the
+			 * attr from the last scan key that came from our scan->keyData[]
+			 * input scan keys.  However, certain implementation restrictions
+			 * might have prevented array preprocessing from doing that for us
+			 * (e.g., opclasses without an "=" operator can't use skip scan).
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -2856,6 +4264,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -2864,6 +4273,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -2883,8 +4293,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -3026,10 +4434,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3095,6 +4504,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -3104,6 +4516,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3177,6 +4605,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3738,6 +5167,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index db6ed784a..86a5de8f4 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 8efb4044d..6efa3e353 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -372,6 +372,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 85e5eaf32..88c929a0a 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -98,6 +98,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 8130f3e8a..256350007 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f73f294b8..cb8470324 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -85,6 +85,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 08fa6774d..42fde7ef2 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -192,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -213,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5736,6 +5740,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6794,6 +6884,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6803,17 +6940,22 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6829,14 +6971,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -6846,13 +6993,90 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't absord a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6874,6 +7098,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6894,7 +7119,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6910,6 +7135,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6925,6 +7182,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -7033,104 +7291,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 5284d23dc..654bda638 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -384,6 +387,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 8a67f0120..6befee3a4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1751,6 +1752,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3590,6 +3602,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index d3358dfc3..fa6f27e6d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1938,7 +1938,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -1958,7 +1958,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 9b2973694..4c4909691 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4440,24 +4440,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -7562,19 +7563,23 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                        QUERY PLAN                         
------------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Nested Loop
    ->  Hash Join
          Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-         ->  Seq Scan on fkest f2
-               Filter: (x100 = 2)
+         ->  Bitmap Heap Scan on fkest f2
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
          ->  Hash
-               ->  Seq Scan on fkest f1
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f1
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
          Index Cond: (x = f1.x)
-(10 rows)
+(14 rows)
 
 alter table fkest add constraint fk
   foreign key (x, x10b, x100) references fkest (x, x10, x100);
@@ -7583,20 +7588,24 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                     QUERY PLAN                      
------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Hash Join
    Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
    ->  Hash Join
          Hash Cond: (f3.x = f2.x)
          ->  Seq Scan on fkest f3
          ->  Hash
-               ->  Seq Scan on fkest f2
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f2
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Hash
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-(11 rows)
+         ->  Bitmap Heap Scan on fkest f1
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
+(15 rows)
 
 rollback;
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 3819bf5e2..58c2d013a 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5240,9 +5240,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index c73631a9a..62eb4239a 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1461,18 +1461,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index fe162cc7c..dea40e013 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -766,7 +766,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -776,7 +776,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 171a7dd5d..b4c2559fb 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2664,6 +2665,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

v13-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/x-patch; name=v13-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From bcdcddc2d76cc57d03664f988afc6dc888c30fb4 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v13 1/2] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 doc/src/sgml/bloom.sgml                       |   6 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 21 files changed, 242 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index e1884acf4..7b4180db5 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -153,6 +153,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index c0b978119..52939d317 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -585,6 +585,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index f2fd62afb..5e423e155 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index b35b8a975..36f1435cb 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index 0d99d6abc..927ba1039 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 60c61039d..ec259012b 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 2919b1263..2933b5830 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -69,6 +69,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -552,6 +553,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -578,6 +580,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -700,6 +703,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			*next_scan_page = btscan->btps_nextScanPage;
 			*last_curr_page = btscan->btps_lastCurrPage;
@@ -789,6 +797,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -823,6 +833,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index d6023732c..3eebc04be 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -970,6 +970,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 301786185..be668abf2 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7c0fd63b2..6cb5ebcd2 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2098,6 +2100,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2111,6 +2114,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2127,6 +2131,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2645,6 +2650,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 19f2b172c..92b13f539 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -170,9 +170,10 @@ CREATE INDEX
    Heap Blocks: exact=28
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..1792.00 rows=2 width=0) (actual time=0.356..0.356 rows=29 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 0.408 ms
-(8 rows)
+(9 rows)
 </programlisting>
   </para>
 
@@ -202,11 +203,12 @@ CREATE INDEX
    -&gt;  BitmapAnd  (cost=24.34..24.34 rows=2 width=0) (actual time=0.027..0.027 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..12.04 rows=500 width=0) (actual time=0.026..0.026 rows=0 loops=1)
                Index Cond: (i5 = 123451)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
-(9 rows)
+(10 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 331315f8d..014a66ef7 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4180,12 +4180,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index cd12b9ce4..482715397 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -727,8 +727,10 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Heap Blocks: exact=10
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 1
  Planning Time: 0.485 ms
  Execution Time: 0.073 ms
 </screen>
@@ -779,6 +781,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Heap Blocks: exact=90
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
  Planning Time: 0.187 ms
  Execution Time: 3.036 ms
 </screen>
@@ -844,6 +847,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -873,9 +877,11 @@ EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique
          Buffers: shared hit=4 read=3
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared hit=2
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
                Buffers: shared hit=2 read=3
  Planning:
    Buffers: shared hit=3
@@ -908,6 +914,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Heap Blocks: exact=90
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1042,6 +1049,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
  Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
  Planning Time: 0.077 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a854..e042638b7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -502,9 +502,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    Batches: 1  Memory Usage: 24kB
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(7 rows)
+(8 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index ae9ce9d8e..c24d56007 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index f6b8329cd..a8bc0bfd7 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7a03b4e36..e3e99272a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2657,47 +2661,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off)
@@ -2713,16 +2726,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2741,7 +2757,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off)
@@ -2757,16 +2773,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2787,7 +2806,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2877,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2897,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,17 +2986,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2982,17 +3013,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3064,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3048,17 +3091,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3112,17 +3161,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3144,17 +3199,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3482,12 +3543,14 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(15);
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3566,10 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(25);
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3617,17 @@ explain (analyze, costs off, summary off, timing off) select * from ma_test wher
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4197,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 33a6dceb0..b7cf35b9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index 2eaeb1477..9afe205e0 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 442428d93..085e746af 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

#43Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Peter Geoghegan (#40)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, 18 Oct 2024 at 18:17, Peter Geoghegan <pg@bowt.ie> wrote:

On Wed, Oct 16, 2024 at 1:14 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v10, which is another revision that's intended only to fix
bit rot against the master branch. There are no notable changes to
report.

Attached is v11, which is yet another revision whose sole purpose is
to fix bit rot/make the patch apply cleanly against the master
branch's tip.

This is a review on v11, not the latest v13. I suspect most comments
still apply, but I haven't verified this.

Re: design

I'm a bit concerned about the additional operations that are being
added to the scan. Before this patch, the amount of work in the
"horizontal" portion of the scan was limited to user-supplied
scankeys, so O(1) even when the index condition is only (f < 7). But,
with this patch, we're adding work for (a=, b=, c=, etc.) for every
tuple in the scan.
As these new "skip array" keys are primarily useful for inter-page
coordination (by determining if we want to start a primitive scan to
skip to a different page and which value range that primitive scan
would search for, or continue on to the next sibling), can't we only
apply the "skip array" portion of the code at the final tuple we
access on this page?

+++ b/doc/src/sgml/indices.sgml

[...]

+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref

I think this last part is a bit more clear about what should go
without the prefix column when formulated as follows:

[...], but this is usually much less efficient than scanning an index
without the extra prefix column.

-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred

While this section already defines some things about index scans which
seem btree-specific, I don't think we should add more references to
btree scan internals in a section about bitmaps and bitmap index
scans. While presumably btree is the most commonly used index type,
I'm not sure if we should just assume that's the only one that does
efficient non-prefix searches. GIN, for example, is quite efficient
for searches on non-primary columns, and BRIN's performance also
generally doesn't care about which column of the index is searched.

+++ b/src/backend/access/nbtree/nbtree.c

[...]

-    slock_t        btps_mutex;        /* protects above variables, btps_arrElems */
+    LWLock        btps_lock;        /* protects above variables, btps_arrElems */

Why is this changed to LWLock, when it's only ever acquired exclusively?

+btestimateparallelscan(Relation rel, int nkeys, int norderbys)

I notice you're using DatumSerialize. Are there reasons why we
wouldn't want to use heap_fill_tuple, which generally produces much
more compact output?

Also, I think you can limit the space usage to BLCKSZ in total,
because a full index tuple can't be larger than 1/3rd of a block; and
for skip scans we'll only have known equality bounds for a prefix of
attributes available in the index tuples, and a single (?)
index-produced dynamic attribute we want to skip ahead of. So, IIUC,
at most we'll have 2 index tuples' worth of data, or 2/3 BLCKSZ.
Right?

+++ b/src/backend/access/nbtree/nbtsearch.c

[...]

+ * Skip scan works by having _bt_preprocess_keys cons up = boundary keys

I needed to look up what this 'cons up' thing is, as it wasn't
something that I'd seen before. It also seems used exclusively in
btree code, and only after the array keys patch, so I think it'd be
better in general to use 'construct' here.

+++ b/src/backend/access/nbtree/nbtcompare.c

The changes here are essentially 6x the same code, but for different
types. What do you think about the attached
0001-Deduplicate[...].patch.txt, which has the same effect but with
only 1 copy of the code checked in?

+++b/src/backend/access/nbtree/nbtutils.c

[...]

+_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts)

Why does this stop processing keys after hitting a row compare?
Doesn't skip scan still apply to any subsequent normal keys? E.g.
"c=1" creates a scan "a=skip, b=skip, c=1", so "(a, b)>(1, 2), c=1"
should IMO still allow a skip scan for a=skip, b=1 to be constructed -
it shouldn't be that we get a much less specific (and potentially,
performant) scan just by adding a rowcompare scankey on early
attributes.

_bt_preprocess_array_keys
-                output_ikey = 0;
+                numArrayKeyData,
+                numSkipArrayKeys;

I don't think numArrayKeyData/arrayKeyData are good names here, as it
confused me many times reviewing this function's changes. On a scankey
of a=1,b=2 we won't have any array keys, yet this variable is set to
2. Something like numOutputKeys is probably more accurate.

+        /* Create a skip array and scan key where indicated by skipatts */
+        while (numSkipArrayKeys &&
+               attno_skip <= scan->keyData[input_ikey].sk_attno)
+        {
+            Oid            opcintype = rel->rd_opcintype[attno_skip - 1];
+            Oid            collation = rel->rd_indcollation[attno_skip - 1];
+            Oid            eq_op = skipatts[attno_skip - 1].eq_op;
+            RegProcedure cmp_proc;
+
+            if (!OidIsValid(eq_op))
+            {
+                /* won't skip using this attribute */
+                attno_skip++;

Isn't this branch impossible, given that numSkipArrayKeys is output
from _bt_decide_skipatts, whose output won't contain skipped
attributes which have eq_op=InvalidOid? I'd replace this with
Assert(OidIsValid(eq_op)).

_bt_rewind_nonrequired_arrays

What types of scan keys can still generate non-required array keys? It
seems to me those are now mostly impossible, as this patch generates
required skip arrays for all attributes that don't yet have an
equality key and which are ahead of any (in)equality keys, except the
case with row compare keys which I already commented on above.

utils/skipsupport.[ch]

I'm not sure why this is included in utils - isn't this exclusively
used in access/nbtree/*?

+++ b/src/include/access/nbtree.h

BTArrayKeyInfo explodes in size, from 24B to 88B. I think some of that
is necessary, but should it really be that large?

Kind regards,

Matthias van de Meent
Neon (https://neon.tech)

Attachments:

0001-Deduplicate-nbtcompare-s-scan-key-support-functions.patch.txttext/plain; charset=US-ASCII; name=0001-Deduplicate-nbtcompare-s-scan-key-support-functions.patch.txtDownload
From 37e59d701698e985007236a374448a6cfcb02f18 Mon Sep 17 00:00:00 2001
From: Matthias van de Meent <boekewurm+postgres@gmail.com>
Date: Sun, 20 Oct 2024 15:34:59 +0200
Subject: [PATCH] Deduplicate nbtcompare's scan key support functions

This saves some 150 lines of duplicated trivial functions.
---
 src/backend/access/nbtree/nbtcompare.c | 357 ++++++-------------------
 src/include/c.h                        |   1 +
 2 files changed, 86 insertions(+), 272 deletions(-)

diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 26ebbf776a..e6af77aae1 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -69,6 +69,91 @@
 #define A_GREATER_THAN_B	1
 #endif
 
+/*
+ * Macros to implement skip support generically across types
+ *
+ * This reduces code duplication for the 6 types we define compare operators
+ * for in this file: bool, int2, int4, int8, oid, and "char".
+ */
+
+#define decrement_fnname(typname) CppConcat(typname, _decrement)
+#define increment_fnname(typname) CppConcat(typname, _increment)
+#define sksup_fnname(typname) CppConcat3(bt, typname, skipsupport)
+
+#define discrete_type_decrement_impl(typ, typname, datname, minvalue) \
+static Datum \
+decrement_fnname(typname)(Relation rel, Datum existing, bool *underflow) \
+{ \
+	typ		t_existing = CppConcat(DatumGet, datname)(existing); \
+\
+	if (t_existing == minvalue) \
+	{ \
+		/* return value is undefined */ \
+		*underflow = true; \
+		return (Datum) 0; \
+	} \
+	\
+	*underflow = false; \
+	return CppConcat(datname, GetDatum)(t_existing - 1); \
+}
+
+#define discrete_type_increment_impl(typ, typname, datname, maxvalue) \
+static Datum \
+increment_fnname(typname)(Relation rel, Datum existing, bool *overflow) \
+{ \
+	typ		t_existing = CppConcat(DatumGet, datname)(existing); \
+\
+	if (t_existing == maxvalue) \
+	{ \
+		/* return value is undefined */ \
+		*overflow = true; \
+		return (Datum) 0; \
+	} \
+	\
+	*overflow = false; \
+	return CppConcat(datname, GetDatum)(t_existing + 1); \
+}
+
+#define discrete_type_skipsupport(typ, typname, datname, minvalue, maxvalue) \
+Datum \
+sksup_fnname(typname)(PG_FUNCTION_ARGS) \
+{ \
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0); \
+\
+	sksup->decrement = decrement_fnname(typname); \
+	sksup->increment = increment_fnname(typname); \
+	sksup->low_elem = CppConcat(datname, GetDatum)(minvalue); \
+	sksup->high_elem = CppConcat(datname, GetDatum)(maxvalue); \
+\
+	PG_RETURN_VOID(); \
+}
+
+/* Actually generate the functions */
+#define impl_discrete_type_sksup(typ, typname, datumname, minvalue, maxvalue) \
+	discrete_type_decrement_impl(typ, typname, datumname, minvalue) \
+	discrete_type_increment_impl(typ, typname, datumname, minvalue) \
+	discrete_type_skipsupport(typ, typname, datumname, minvalue, maxvalue)
+
+/* Implement for various types */
+impl_discrete_type_sksup(int16, int2, Int16, PG_INT16_MIN, PG_INT16_MAX)
+impl_discrete_type_sksup(int32, int4, Int32, PG_INT32_MIN, PG_INT32_MAX)
+impl_discrete_type_sksup(int64, int8, Int64, PG_INT64_MIN, PG_INT64_MAX)
+impl_discrete_type_sksup(Oid, oid, ObjectId, InvalidOid, OID_MAX)
+impl_discrete_type_sksup(uint8, char, UInt8, 0, PG_UINT8_MAX)
+
+/*
+ * If bool is a #define (e.g. on _Bool), the macro substitution will fail,
+ * so we substitute the #define with a true typedef here, so that macro
+ * expansion will use bool instead of e.g. _Bool in the function names.
+ */
+#ifdef bool
+typedef bool substitute_bool;
+#undef bool
+typedef substitute_bool bool;
+#endif
+
+impl_discrete_type_sksup(bool, bool, Bool, false, true)
+
 
 Datum
 btboolcmp(PG_FUNCTION_ARGS)
@@ -79,51 +164,6 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
-static Datum
-bool_decrement(Relation rel, Datum existing, bool *underflow)
-{
-	bool		bexisting = DatumGetBool(existing);
-
-	if (bexisting == false)
-	{
-		/* return value is undefined */
-		*underflow = true;
-		return (Datum) 0;
-	}
-
-	*underflow = false;
-	return BoolGetDatum(bexisting - 1);
-}
-
-static Datum
-bool_increment(Relation rel, Datum existing, bool *overflow)
-{
-	bool		bexisting = DatumGetBool(existing);
-
-	if (bexisting == true)
-	{
-		/* return value is undefined */
-		*overflow = true;
-		return (Datum) 0;
-	}
-
-	*overflow = false;
-	return BoolGetDatum(bexisting + 1);
-}
-
-Datum
-btboolskipsupport(PG_FUNCTION_ARGS)
-{
-	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
-
-	sksup->decrement = bool_decrement;
-	sksup->increment = bool_increment;
-	sksup->low_elem = BoolGetDatum(false);
-	sksup->high_elem = BoolGetDatum(true);
-
-	PG_RETURN_VOID();
-}
-
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -151,51 +191,6 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
-static Datum
-int2_decrement(Relation rel, Datum existing, bool *underflow)
-{
-	int16		iexisting = DatumGetInt16(existing);
-
-	if (iexisting == PG_INT16_MIN)
-	{
-		/* return value is undefined */
-		*underflow = true;
-		return (Datum) 0;
-	}
-
-	*underflow = false;
-	return Int16GetDatum(iexisting - 1);
-}
-
-static Datum
-int2_increment(Relation rel, Datum existing, bool *overflow)
-{
-	int16		iexisting = DatumGetInt16(existing);
-
-	if (iexisting == PG_INT16_MAX)
-	{
-		/* return value is undefined */
-		*overflow = true;
-		return (Datum) 0;
-	}
-
-	*overflow = false;
-	return Int16GetDatum(iexisting + 1);
-}
-
-Datum
-btint2skipsupport(PG_FUNCTION_ARGS)
-{
-	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
-
-	sksup->decrement = int2_decrement;
-	sksup->increment = int2_increment;
-	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
-	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
-
-	PG_RETURN_VOID();
-}
-
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -219,51 +214,6 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
-static Datum
-int4_decrement(Relation rel, Datum existing, bool *underflow)
-{
-	int32		iexisting = DatumGetInt32(existing);
-
-	if (iexisting == PG_INT32_MIN)
-	{
-		/* return value is undefined */
-		*underflow = true;
-		return (Datum) 0;
-	}
-
-	*underflow = false;
-	return Int32GetDatum(iexisting - 1);
-}
-
-static Datum
-int4_increment(Relation rel, Datum existing, bool *overflow)
-{
-	int32		iexisting = DatumGetInt32(existing);
-
-	if (iexisting == PG_INT32_MAX)
-	{
-		/* return value is undefined */
-		*overflow = true;
-		return (Datum) 0;
-	}
-
-	*overflow = false;
-	return Int32GetDatum(iexisting + 1);
-}
-
-Datum
-btint4skipsupport(PG_FUNCTION_ARGS)
-{
-	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
-
-	sksup->decrement = int4_decrement;
-	sksup->increment = int4_increment;
-	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
-	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
-
-	PG_RETURN_VOID();
-}
-
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -307,51 +257,6 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
-static Datum
-int8_decrement(Relation rel, Datum existing, bool *underflow)
-{
-	int64		iexisting = DatumGetInt64(existing);
-
-	if (iexisting == PG_INT64_MIN)
-	{
-		/* return value is undefined */
-		*underflow = true;
-		return (Datum) 0;
-	}
-
-	*underflow = false;
-	return Int64GetDatum(iexisting - 1);
-}
-
-static Datum
-int8_increment(Relation rel, Datum existing, bool *overflow)
-{
-	int64		iexisting = DatumGetInt64(existing);
-
-	if (iexisting == PG_INT64_MAX)
-	{
-		/* return value is undefined */
-		*overflow = true;
-		return (Datum) 0;
-	}
-
-	*overflow = false;
-	return Int64GetDatum(iexisting + 1);
-}
-
-Datum
-btint8skipsupport(PG_FUNCTION_ARGS)
-{
-	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
-
-	sksup->decrement = int8_decrement;
-	sksup->increment = int8_increment;
-	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
-	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
-
-	PG_RETURN_VOID();
-}
-
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -473,51 +378,6 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
-static Datum
-oid_decrement(Relation rel, Datum existing, bool *underflow)
-{
-	Oid			oexisting = DatumGetObjectId(existing);
-
-	if (oexisting == InvalidOid)
-	{
-		/* return value is undefined */
-		*underflow = true;
-		return (Datum) 0;
-	}
-
-	*underflow = false;
-	return ObjectIdGetDatum(oexisting - 1);
-}
-
-static Datum
-oid_increment(Relation rel, Datum existing, bool *overflow)
-{
-	Oid			oexisting = DatumGetObjectId(existing);
-
-	if (oexisting == OID_MAX)
-	{
-		/* return value is undefined */
-		*overflow = true;
-		return (Datum) 0;
-	}
-
-	*overflow = false;
-	return ObjectIdGetDatum(oexisting + 1);
-}
-
-Datum
-btoidskipsupport(PG_FUNCTION_ARGS)
-{
-	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
-
-	sksup->decrement = oid_decrement;
-	sksup->increment = oid_increment;
-	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
-	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
-
-	PG_RETURN_VOID();
-}
-
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -551,50 +411,3 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
-
-static Datum
-char_decrement(Relation rel, Datum existing, bool *underflow)
-{
-	uint8		cexisting = UInt8GetDatum(existing);
-
-	if (cexisting == 0)
-	{
-		/* return value is undefined */
-		*underflow = true;
-		return (Datum) 0;
-	}
-
-	*underflow = false;
-	return CharGetDatum((uint8) cexisting - 1);
-}
-
-static Datum
-char_increment(Relation rel, Datum existing, bool *overflow)
-{
-	uint8		cexisting = UInt8GetDatum(existing);
-
-	if (cexisting == UCHAR_MAX)
-	{
-		/* return value is undefined */
-		*overflow = true;
-		return (Datum) 0;
-	}
-
-	*overflow = false;
-	return CharGetDatum((uint8) cexisting + 1);
-}
-
-Datum
-btcharskipsupport(PG_FUNCTION_ARGS)
-{
-	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
-
-	sksup->decrement = char_decrement;
-	sksup->increment = char_increment;
-
-	/* btcharcmp compares chars as unsigned */
-	sksup->low_elem = UInt8GetDatum(0);
-	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
-
-	PG_RETURN_VOID();
-}
diff --git a/src/include/c.h b/src/include/c.h
index 55dec71a6d..cbf6499151 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -329,6 +329,7 @@
 #define CppAsString(identifier) #identifier
 #define CppAsString2(x)			CppAsString(x)
 #define CppConcat(x, y)			x##y
+#define CppConcat3(x, y, z)		x##y##z
 
 /*
  * VA_ARGS_NARGS
-- 
2.46.0

In reply to: Matthias van de Meent (#43)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Mon, Nov 4, 2024 at 4:58 PM Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:

This is a review on v11, not the latest v13. I suspect most comments
still apply, but I haven't verified this.

v11 is indeed quite similar to v13, so this shouldn't really matter.

I'm a bit concerned about the additional operations that are being
added to the scan. Before this patch, the amount of work in the
"horizontal" portion of the scan was limited to user-supplied
scankeys, so O(1) even when the index condition is only (f < 7). But,
with this patch, we're adding work for (a=, b=, c=, etc.) for every
tuple in the scan.

There's no question that there are still some cases where this cannot
possibly pay for itself. And that just isn't acceptable -- no
arguments here.

As these new "skip array" keys are primarily useful for inter-page
coordination (by determining if we want to start a primitive scan to
skip to a different page and which value range that primitive scan
would search for, or continue on to the next sibling), can't we only
apply the "skip array" portion of the code at the final tuple we
access on this page?

I plan on doing something like that. I'll need to.

AFAICT I only need to avoid wasting CPU cycles here -- there are no
notable regressions from performing excessive amounts of index
descents, as far as I can tell. And so I plan on doing this without
fundamentally changing anything about the current design.

In particular, I want the performance to remain robust in cases where
the best strategy varies significantly as the scan progresses -- even
when we need to strongly favor skipping at first, and then strongly
favor staying/not skipping later on (all during the same individual
index scan). I really like that the current design gets that part
right.

While this section already defines some things about index scans which
seem btree-specific, I don't think we should add more references to
btree scan internals in a section about bitmaps and bitmap index
scans.

This section of the docs discusses the trade-off between multiple
single column indexes, and fewer multi-column indexes. How could skip
scan not be relevant to such a discussion? One of the main benefits of
skip scan is that it'll allow users to get by with fewer indexes.

+++ b/src/backend/access/nbtree/nbtree.c

[...]

-    slock_t        btps_mutex;        /* protects above variables, btps_arrElems */
+    LWLock        btps_lock;        /* protects above variables, btps_arrElems */

Why is this changed to LWLock, when it's only ever acquired exclusively?

In general one should never do more than an extremely small, tightly
controlled amount of work with a spinlock held. It's now possible that
we'll allocate memory with the lock held -- doing that with a spinlock
held is an obvious no-no. We really need to use an LWLock for this
stuff now, on general principle.

+btestimateparallelscan(Relation rel, int nkeys, int norderbys)

I notice you're using DatumSerialize. Are there reasons why we
wouldn't want to use heap_fill_tuple, which generally produces much
more compact output?

heap_fill_tuple doesn't support the notion of -inf and +inf scan key
sentinel values. Plus I'm inclined to use DatumSerialize because it's
more or less designed for this kind of problem.

Also, I think you can limit the space usage to BLCKSZ in total,
because a full index tuple can't be larger than 1/3rd of a block; and
for skip scans we'll only have known equality bounds for a prefix of
attributes available in the index tuples, and a single (?)
index-produced dynamic attribute we want to skip ahead of. So, IIUC,
at most we'll have 2 index tuples' worth of data, or 2/3 BLCKSZ.
Right?

Possibly, but who wants to take a chance? The scheme you're describing
only saves memory when there's 3 skip arrays, which is fairly unlikely
in general.

I think that the approach taken to serializing the array keys should
be as conservative as possible. It's not particularly likely that
we'll want to do a parallel skip scan. It's rather hard to test those
code paths.

I needed to look up what this 'cons up' thing is, as it wasn't
something that I'd seen before. It also seems used exclusively in
btree code, and only after the array keys patch, so I think it'd be
better in general to use 'construct' here.

FWIW I wasn't the first person to use the term in the nbtree code.

I think you're right, though. It is a needlessly obscure term that is
only known to Lisp hackers. I'll fix it.

+++ b/src/backend/access/nbtree/nbtcompare.c

The changes here are essentially 6x the same code, but for different
types. What do you think about the attached
0001-Deduplicate[...].patch.txt, which has the same effect but with
only 1 copy of the code checked in?

Reminds me of the approach taken by this extension:

https://github.com/petere/pguint

I do find the need to write so much boilerplate code for B-Tree
opclasses annoying. I also find it annoying that the nbtree code
insists on being as forgiving as possible with incomplete opfamilies.
But those problems seem out of scope here -- not like I'm really
making it any worse.

+++b/src/backend/access/nbtree/nbtutils.c

[...]

+_bt_decide_skipatts(IndexScanDesc scan, BTSkipPreproc *skipatts)

Why does this stop processing keys after hitting a row compare?

Why not? It's not ideal, but there are a number of things about
RowCompare scan keys that are already less than ideal. We don't even
try to do any kind of preprocessing that involves RowCompares --
they're already a slightly awkward special case to the nbtree code.

Doesn't skip scan still apply to any subsequent normal keys? E.g.
"c=1" creates a scan "a=skip, b=skip, c=1", so "(a, b)>(1, 2), c=1"
should IMO still allow a skip scan for a=skip, b=1 to be constructed -
it shouldn't be that we get a much less specific (and potentially,
performant) scan just by adding a rowcompare scankey on early
attributes.

It's just awkward to get it to work as expected, while still
preserving all of the useful properties of the design.

Your "(a, b)>(1,2)" scan key returns rows matching:

WHERE (a = 1 AND b > 2) OR (a > 1)

And so your complete "(a, b)>(1,2) AND c = 1" qual returns rows matching:

WHERE ((a = 1 AND b > 2) OR (a > 1)) AND c = 1

It is difficult to imagine how the existing design for skip arrays can
be extended to support this kind of qual, though. I guess we'd still
need skip arrays on both "a" and "b" here, though. Right?

The "b" skip array would be restricted to a range of values between 3
and +inf inclusive, if and only if we were still on the "a" skip
array's first array element (i.e. iff "a = 1" the "b" has a valid
low_compare). Otherwise (i.e. when "a > 1"), the "b" skip array
wouldn't be constrained by any low_compare inequality. So
low_compare/high_compare only apply conditionally, in a world where we
support these kinds of RowCompare quals. Right now I can avoid the
problem by refusing to allow "c = 1" to ever be marked required (by
never creating any skip array on an index attribute >= an attribute
with a RowCompare key on input).

Obviously, the current design of skip arrays involves arrays that are
always constrained by the same range/low_compare and high_compare
inequalities, independent of any other factor/any wider context. It's
not impossible to make something like your RowCompare case work, but
it'd be very significantly more complicated than the existing design.
Though not all that much more useful.

Doing something like this might make sense in the context of a project
that adds support for the MDAM paper's "General OR Optimization"
transformations -- RowCompare support would only be a bonus. I don't
see any opportunities to target RowCompare as independent work --
seems as if RowCompare quals aren't significantly simpler than what is
required to support very general MDAM OR optimizations.

_bt_preprocess_array_keys
-                output_ikey = 0;
+                numArrayKeyData,
+                numSkipArrayKeys;

I don't think numArrayKeyData/arrayKeyData are good names here, as it
confused me many times reviewing this function's changes.

The code in this area actually did change recently, though I didn't announce it.

On a scankey
of a=1,b=2 we won't have any array keys, yet this variable is set to
2. Something like numOutputKeys is probably more accurate.

That's not accurate. _bt_preprocess_array_keys() sets
*new_numberOfKeys on output. When there are no array keys (of either
type) to be output, _bt_preprocess_array_keys will just return NULL --
it won't have changed the *new_numberOfKeys passed by its
_bt_preprocess_keys caller when it returns NULL.

In other words, when _bt_preprocess_array_keys determines that there
are no array keys to process (neither SAOP arrays nor skip arrays), it
won't modify anything, and won't return an alternative
input-to-_bt_preprocess_keys scan key array. _bt_preprocess_keys will
work directly off of the scan->keyData[] input scan keys passed by the
executor proper.

+        /* Create a skip array and scan key where indicated by skipatts */
+        while (numSkipArrayKeys &&
+               attno_skip <= scan->keyData[input_ikey].sk_attno)
+        {
+            Oid            opcintype = rel->rd_opcintype[attno_skip - 1];
+            Oid            collation = rel->rd_indcollation[attno_skip - 1];
+            Oid            eq_op = skipatts[attno_skip - 1].eq_op;
+            RegProcedure cmp_proc;
+
+            if (!OidIsValid(eq_op))
+            {
+                /* won't skip using this attribute */
+                attno_skip++;

Isn't this branch impossible, given that numSkipArrayKeys is output
from _bt_decide_skipatts, whose output won't contain skipped
attributes which have eq_op=InvalidOid? I'd replace this with
Assert(OidIsValid(eq_op)).

It's very possible to hit this code path -- we'll hit this code path
every time an explicit "=" input scan key appears before an attribute
that requires a skip array (which happens whenever there's a third,
later column that we'll be able to mark required thanks to the skip
array).

Note that BTSkipPreproc.eq_op is the "= op to be used to add array, if
any". And so when we see !OidIsValid(eq_op) in this loop, it means
"this is for an index attribute that we don't want to add a skip array
to" (though, as I said, a later attribute is expected to get a skip
array when this happens).

_bt_rewind_nonrequired_arrays

What types of scan keys can still generate non-required array keys?

Right now it's:

1. As you said, certain cases involving RowCompare scan keys.
2. Cases involving B-Tree operator classes that don't even have a "="
operator (yes, technically those are supported!).

Also seems possible that I'll end up relying on our support for
non-required arrays when I go fix those regressions. Seems possible
that I'll get rid of the explicit SK_BT_REQFWD/SK_BT_REQBKWD markings,
and go back to treating scan keys as required based on context (a
little like how things were prior to commit 7ccaf13a06).

It seems to me those are now mostly impossible, as this patch generates
required skip arrays for all attributes that don't yet have an
equality key and which are ahead of any (in)equality keys, except the
case with row compare keys which I already commented on above.

I agree that non-required arrays become an obscure edge case with the
patch, having been reasonably common before now (well, they were
common in Postgres 17). I don't think that that provides us with any
opportunities to get rid of unneeded code.

utils/skipsupport.[ch]

I'm not sure why this is included in utils - isn't this exclusively
used in access/nbtree/*?

The location of skip support is based on (though slightly different
to) the location of sort support. In general many B-Tree opclasses are
implemented in or around src/utils/adt/*. The exception is all of the
stuff in nbtcompare.c, though I always found that weird (I wouldn't
mind getting rid of nbtcompare.c by relocating its code places like
int.c and int8.c).

+++ b/src/include/access/nbtree.h

BTArrayKeyInfo explodes in size, from 24B to 88B. I think some of that
is necessary, but should it really be that large?

I'm disinclined to do anything about it right now.

I'll make a note of it, and review when the most important performance
problems are fixed.

Thanks for the review
--
Peter Geoghegan

In reply to: Peter Geoghegan (#42)
2 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Mon, Nov 4, 2024 at 11:41 AM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v13, which is the first revision posted that has new
functional changes in quite some time (this isn't just for fixing
bitrot).

Attached is v14, which is just to fix bitrot caused by today's bugfix
commit b5ee4e52. Nothing new to report here.

--
Peter Geoghegan

Attachments:

v14-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v14-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From d1ab58f20a481fc159ec05eab41213d6a6827c4f Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v14 1/2] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 doc/src/sgml/bloom.sgml                       |   6 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 21 files changed, 242 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index e1884acf4..7b4180db5 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -153,6 +153,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index c0b978119..52939d317 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -585,6 +585,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index f2fd62afb..5e423e155 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index b35b8a975..36f1435cb 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index 0d99d6abc..927ba1039 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 60c61039d..ec259012b 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index dd76fe1da..8bbb3d734 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -69,6 +69,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -552,6 +553,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -578,6 +580,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -705,6 +708,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			Assert(btscan->btps_nextScanPage != P_NONE);
 			*next_scan_page = btscan->btps_nextScanPage;
@@ -805,6 +813,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -839,6 +849,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 2786a8564..79329aaa9 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -969,6 +969,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 301786185..be668abf2 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7c0fd63b2..6cb5ebcd2 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2098,6 +2100,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2111,6 +2114,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2127,6 +2131,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2645,6 +2650,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 19f2b172c..92b13f539 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -170,9 +170,10 @@ CREATE INDEX
    Heap Blocks: exact=28
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..1792.00 rows=2 width=0) (actual time=0.356..0.356 rows=29 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 0.408 ms
-(8 rows)
+(9 rows)
 </programlisting>
   </para>
 
@@ -202,11 +203,12 @@ CREATE INDEX
    -&gt;  BitmapAnd  (cost=24.34..24.34 rows=2 width=0) (actual time=0.027..0.027 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..12.04 rows=500 width=0) (actual time=0.026..0.026 rows=0 loops=1)
                Index Cond: (i5 = 123451)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
-(9 rows)
+(10 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 331315f8d..014a66ef7 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4180,12 +4180,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index cd12b9ce4..482715397 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -727,8 +727,10 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Heap Blocks: exact=10
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 1
  Planning Time: 0.485 ms
  Execution Time: 0.073 ms
 </screen>
@@ -779,6 +781,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Heap Blocks: exact=90
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
  Planning Time: 0.187 ms
  Execution Time: 3.036 ms
 </screen>
@@ -844,6 +847,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -873,9 +877,11 @@ EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique
          Buffers: shared hit=4 read=3
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared hit=2
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
                Buffers: shared hit=2 read=3
  Planning:
    Buffers: shared hit=3
@@ -908,6 +914,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Heap Blocks: exact=90
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1042,6 +1049,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
  Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
  Planning Time: 0.077 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a854..e042638b7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -502,9 +502,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    Batches: 1  Memory Usage: 24kB
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(7 rows)
+(8 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index ae9ce9d8e..c24d56007 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index f6b8329cd..a8bc0bfd7 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7a03b4e36..e3e99272a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2657,47 +2661,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off)
@@ -2713,16 +2726,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2741,7 +2757,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off)
@@ -2757,16 +2773,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2787,7 +2806,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2877,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2897,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,17 +2986,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2982,17 +3013,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3064,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3048,17 +3091,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3112,17 +3161,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3144,17 +3199,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3482,12 +3543,14 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(15);
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3566,10 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(25);
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3617,17 @@ explain (analyze, costs off, summary off, timing off) select * from ma_test wher
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4197,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 33a6dceb0..b7cf35b9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index 2eaeb1477..9afe205e0 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 442428d93..085e746af 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

v14-0002-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v14-0002-Add-skip-scan-to-nbtree.patchDownload
From 583305ec583eab506da50eabfc8c608a7f662da9 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v14 2/2] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
useful in cases where the total number of distinct values in the column
'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.

The design of skip arrays allows array advancement to work without
concern for which specific array types are involved; only the lowest
level code needs to treat skip arrays and SAOP arrays differently.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values are conceptually just like any
other value that might appear in either kind of array.  A call made to
_bt_first to reposition the scan based on a NEXT/PRIOR sentinel usually
won't locate a leaf page containing tuples that must be returned to the
scan, but that's normal during any skip scan that happens to involve
"sparse" indexed values (skip support can only help with "dense" indexed
values, which isn't meaningfully possible when the skipped column is of
a continuous type, where skip support typically can't be implemented).

The optimizer doesn't use distinct new index paths to represent index
skip scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  Skipping is possible during
eligible bitmap index scans, index scans, and index-only scans.  It's
also possible during eligible parallel scans.  An eligible scan is any
scan that nbtree preprocessing generates a skip array for, which happens
whenever doing so will enable later preprocessing to mark original input
scan keys (passed by the executor) as required to continue the scan.

There are hardly any limitations around where skip arrays/scan keys may
appear relative to conventional/input scan keys.  This is no less true
in the presence of conventional SAOP array scan keys, which may both
roll over and be rolled over by skip arrays.  For example, a skip array
on the column "b" is generated for "WHERE a = 42 AND c IN (5, 6, 7, 8)".
As always (since commit 5bf748b8), whether or not nbtree actually skips
depends in large part on physical index characteristics at runtime.
Preprocessing is optimistic about skipping working out: it applies
simple static rules to decide where to place skip arrays.  It's up to
array-related scan code to keep the overhead of maintaining the scan's
skip arrays under control when it turns out that skipping isn't helpful.

Preprocessing will never cons up a skip array for an index column that
has an equality strategy scan key on input, but will do so for indexed
columns that are only constrained by inequality strategy scan keys.
This allows the scan to skip using a range skip array.  These are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   28 +-
 src/include/catalog/pg_amproc.dat             |   16 +
 src/include/catalog/pg_proc.dat               |   24 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  101 +
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  273 +++
 src/backend/access/nbtree/nbtree.c            |  208 +-
 src/backend/access/nbtree/nbtsearch.c         |   75 +-
 src/backend/access/nbtree/nbtutils.c          | 1705 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   46 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  379 +++-
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/uuid.c                  |   71 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/btree.sgml                       |   34 +-
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   49 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |   10 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   61 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    5 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 34 files changed, 2924 insertions(+), 332 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c51de742e..0826c124f 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -202,7 +202,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 123fba624..d841e85bc 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1022,10 +1024,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* skip scan support (unless !use_sksup) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo   *low_order;		/* low_compare's ORDER proc */
+	FmgrInfo   *high_order;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1113,6 +1127,10 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on omitted prefix column */
+#define SK_BT_NEGPOSINF	0x00080000	/* -inf/+inf key (invalid sk_argument) */
+#define SK_BT_NEXT		0x00100000	/* positions scan > sk_argument */
+#define SK_BT_PRIOR		0x00200000	/* positions scan < sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1149,6 +1167,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1159,7 +1181,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5d7fe292b..c0ccdfdfb 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -122,6 +128,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +149,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +170,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +205,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +275,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f23321a41..99a73eca0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2260,6 +2275,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4447,6 +4465,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -9317,6 +9338,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d70e6d37e..5b58739ad 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..d97e6059d
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,101 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining pairs of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code falls back on next-key sentinel values for any opclass that
+ * doesn't provide its own skip support function.  There is no benefit to
+ * providing skip support for an opclass on a type where guessing that the
+ * next indexed value is the next possible indexable value seldom works out.
+ * It might not even be feasible to add skip support for a continuous type.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order).
+	 * This gives the B-Tree code a useful value to start from, before any
+	 * data has been read from the index.  These are also sometimes used to
+	 * detect when the scan has run out of indexable values to search for.
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 *
+	 * Operator class decrement/increment functions never have to directly
+	 * deal with the value NULL, nor with ASC vs. DESC column ordering.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 1859be614..b4f1466d4 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..26ebbf776 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 8bbb3d734..c0c45f904 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -29,6 +29,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -70,7 +71,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -78,11 +79,23 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -535,10 +548,156 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
 	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Assume every index attribute might require that we generate a skip scan
+	 * key
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		Form_pg_attribute attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array/skip scan key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array/skip scan key, while freeing memory allocated
+		 * for old sk_argument where required
+		 */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -549,7 +708,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -572,16 +732,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -608,6 +768,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -652,7 +813,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -674,14 +835,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -719,7 +875,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -763,11 +919,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -806,7 +962,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -815,7 +971,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -833,6 +989,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -842,7 +999,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -852,14 +1009,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 79329aaa9..8966f4405 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -983,6 +983,13 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  Skip
+	 * array keys are = keys, though we'll sometimes need to treat the key as
+	 * if it was some kind of inequality instead.  This is required whenever
+	 * the skip array's current array element is a sentinel value.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1058,8 +1065,39 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is -inf or +inf, choose low_compare/high_compare
+				 * (or consider if they imply a NOT NULL constraint).
+				 */
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skiparraysk = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					if (!array->null_elem)
+						impliesNN = skiparraysk;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1096,6 +1134,39 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					strat_total == BTLessStrategyNumber)
 					break;
 
+				/*
+				 * If this is a scan key for a skip array whose current
+				 * element is marked NEXT or PRIOR, adjust strat_total
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(strat_total == BTEqualStrategyNumber);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[].
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).
+					 */
+					break;
+				}
+
 				/*
 				 * Done if that was the last attribute, or if next key is not
 				 * in sequence (implying no boundary key is available for the
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index d76032502..c57cedbc6 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	Oid			eq_op;			/* = op to be used to add array, if any */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -63,16 +91,46 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
+static int	_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+										  int *numSkipArrayKeys);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_decrement(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_increment(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -256,6 +314,12 @@ _bt_freestack(BTStack stack)
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -271,10 +335,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -282,30 +348,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_preprocess_num_array_keys(scan, skipatts,
+												 &numSkipArrayKeys);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys described within skipatts[]
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -320,17 +380,18 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/* Process input keys, and emit skip array keys described by skipatts[] */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -346,19 +407,97 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and its scan key where indicated */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* attribute already had an "=" key on input */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[numArrayKeyData];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].low_order = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].high_order = NULL;	/* for now */
+
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how the
+			 * consed-up "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			numArrayKeyData++;	/* keep this scan key/array */
+		}
+
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
+		cur = &arrayKeyData[numArrayKeyData];
 		*cur = scan->keyData[input_ikey];
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -414,7 +553,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -425,7 +564,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -442,7 +581,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -517,23 +656,232 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].low_order = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].high_order = NULL;	/* unused */
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
 	return arrayKeyData;
 }
 
+/*
+ *	_bt_preprocess_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit all required so->arrayKeys[] entries.
+ *
+ * Also sets *numSkipArrayKeys to the number of skip arrays that caller must
+ * make sure to add to the scan keys it'll output (complete with corresponding
+ * so->arrayKeys[] entries), and sets the corresponding attributes positions
+ * in *skipatts argument.  Every attribute that receives a skip array will
+ * have its skip support routine set in *skipatts, too.
+ */
+static int
+_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+							  int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_inkey = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+		int			prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				(*numSkipArrayKeys)++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				break;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatt
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatt->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										 BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack even an equality strategy
+	 * operator, but they're supported.  We cope by making caller not use a
+	 * skip array for this index column (nor for any lower-order columns).
+	 */
+	if (!OidIsValid(skipatt->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatt->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+													   reverse,
+													   &skipatt->sksup);
+
+	/* might not have set up skip support, but can skip either way */
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys_final() -- fix up array scan key references
  *
@@ -633,7 +981,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -669,6 +1018,14 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && !array->null_elem)
+						_bt_skip_preproc_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
@@ -684,7 +1041,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -980,26 +1337,28 @@ _bt_merge_arrays(IndexScanDesc scan, ScanKey skey, FmgrInfo *sortproc,
  * guaranteeing that at least the scalar scan key can be considered redundant.
  * We return false if the comparison could not be made (caller must keep both
  * scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar inequality scan keys.
  */
 static bool
 _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
 	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
 		   skey->sk_strategy != BTEqualStrategyNumber);
@@ -1009,8 +1368,7 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1041,11 +1399,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensures that the scalar scan key can be eliminated as redundant
+		 */
+		eliminated = true;
+
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+	}
+	else
+	{
+		/*
+		 * With a skip array, it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when there's a lack of suitable cross-type support for
+		 * comparing skey to an existing inequality associated with the array.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when
+ * determining its redundancy against a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1097,10 +1509,305 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of a skip array scan key when determining its
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array.  This forces the array's elements to be generated within the limits
+ * of a range that's described/constrained by the array's inequalities.
+ *
+ * Return value indicates if the scalar scan key could be "eliminated".  We
+ * return true in the common case where caller's scan key was successfully
+ * rolled into the skip array.  We return false when we can't do that due to
+ * the presence of an existing, conflicting inequality that cannot be compared
+ * to caller's inequality due to a lack of suitable cross-type support.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way scan code can generally assume that
+	 * it'll always be safe to use the ORDER procs stored in so->orderProcs[].
+	 * The only exception is cases where the array's scan key is NEGPOSINF.
+	 *
+	 * When the array's scan key is marked NEGPOSINF, then it'll lack a valid
+	 * sk_argument.  The scan must apply the array's low_compare and/or
+	 * high_compare (if any) to establish whether a given tupdatum is within
+	 * the range of the skip array.  This could involve applying the 3-way
+	 * low_order/high_order ORDER proc we store here (though never the ORDER
+	 * proc stored in so->orderProcs[]).
+	 *
+	 * Note: we sometimes use low_compare/high_compare inequalities with the
+	 * array's current element value, and with values that were mutated by the
+	 * array's skip support routine.  A skip array must always use its index
+	 * attribute's own input opclass/type, so this will always work correctly.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (!array->high_compare)
+			{
+				/* array currently lacks a high_compare, make space for one */
+				array->high_compare = palloc(sizeof(ScanKeyData));
+				array->high_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare, keep old one */
+
+				/* replace old high_compare with caller's new one */
+			}
+
+			/* caller's high_compare becomes skip array's high_compare */
+			*array->high_compare = *skey;
+			*array->high_order = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (!array->low_compare)
+			{
+				/* array currently lacks a low_compare, make space for one */
+				array->low_compare = palloc(sizeof(ScanKeyData));
+				array->low_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare, keep old one */
+
+				/* replace old low_compare with caller's new one */
+			}
+
+			/* caller's low_compare becomes skip array's low_compare */
+			*array->low_compare = *skey;
+			*array->low_order = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
 
+/*
+ * Perform final preprocessing for a skip array.  Called when the skip array's
+ * final low_compare and high_compare have been chosen.
+ *
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts the
+ * operator strategies).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value NEGPOSINF, using >= instead of using >
+ * (or using <= instead of < during backwards scans) makes it safe to include
+ * lower-order scan keys in the insertion scan key (there must be lower-order
+ * scan keys after the skip array).  We will avoid an extra _bt_first to find
+ * the first value in the index > sk_argument, at least when the first real
+ * matching value in the index happens to be an exact match for the newly
+ * transformed (newly incremented/decremented) sk_argument value.
+ */
+static void
+_bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+	Assert(!array->null_elem);
+
+	/* If skip array doesn't have skip support, can't apply optimization */
+	if (!array->use_sksup)
+		return;
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skip_preproc_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skip_preproc_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skip_preproc_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	bool		reverse = (arraysk->sk_flags & SK_BT_DESC) != 0;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
+	Oid			sktype = opcintype;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	SkipSupport ss = &array->sksup;
+	SkipSupportData sksup;
+	Oid			cmp_op;
+	RegProcedure cmp_proc;
+	bool		underflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+
+	/*
+	 * Note: we have to support the convention that sk_subtype == InvalidOid
+	 * means the opclass input type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+	{
+		/* hard case: have to look up separate skip support routine */
+		ss = &sksup;
+		sktype = high_compare->sk_subtype;
+		if (!PrepareSkipSupportFromOpclass(opfamily, sktype, reverse, ss))
+			return;
+
+		/* successfully looked up other opclass' skip support routine */
+	}
+
+	/*
+	 * Look up <= operator (might fail), then decrement (might underflow).
+	 *
+	 * Note: It might be possible to handle underflow by marking the whole
+	 * qual unsatisfiable, but we just back out instead.  This keeps things
+	 * simple, particularly in cross-type scenarios.
+	 */
+	cmp_op = get_opfamily_member(opfamily, opcintype, sktype,
+								 BTLessEqualStrategyNumber);
+	if (!OidIsValid(cmp_op))
+		return;
+	new_sk_argument = ss->decrement(rel, orig_sk_argument, &underflow);
+	if (underflow)
+		return;
+
+	cmp_proc = get_opcode(cmp_op);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skip_preproc_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	bool		reverse = (arraysk->sk_flags & SK_BT_DESC) != 0;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
+	Oid			sktype = opcintype;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	SkipSupport ss = &array->sksup;
+	SkipSupportData sksup;
+	Oid			cmp_op;
+	RegProcedure cmp_proc;
+	bool		overflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+
+	/*
+	 * Note: we have to support the convention that sk_subtype == InvalidOid
+	 * means the opclass input type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+	{
+		/* hard case: have to look up separate skip support routine */
+		ss = &sksup;
+		sktype = low_compare->sk_subtype;
+		if (!PrepareSkipSupportFromOpclass(opfamily, sktype, reverse, ss))
+			return;
+
+		/* successfully looked up other opclass' skip support routine */
+	}
+
+	/*
+	 * Look up >= operator (might fail), then increment (might overflow).
+	 *
+	 * Note: It might be possible to handle overflow by marking the whole qual
+	 * unsatisfiable, but we just back out instead.  This keeps things simple,
+	 * particularly in cross-type scenarios.
+	 */
+	cmp_op = get_opfamily_member(opfamily, opcintype, sktype,
+								 BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(cmp_op))
+		return;
+	new_sk_argument = ss->increment(rel, orig_sk_argument, &overflow);
+	if (overflow)
+		return;
+
+	cmp_proc = get_opcode(cmp_op);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  * qsort_arg comparator for sorting array elements
  */
@@ -1220,6 +1927,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1342,6 +2051,98 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, because the array doesn't really
+ * have any elements (it generates its array elements procedurally instead).
+ * Note that this may include a NULL value/an IS NULL qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ *
+ * Note: This function doesn't actually binary search.  It uses an interface
+ * that's as similar to _bt_binsrch_array_skey as possible for consistency.
+ * "Binary searching a skip array" just means determining if tupdatum/tupnull
+ * are before, within, or beyond the range of caller's skip array.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1351,29 +2152,482 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting skip array to lowest element, which isn't just NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Setting skip array to highest element, which isn't just NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		underflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element is out of bounds for the array.
+		 *
+		 * This is only possible if low_compare represents an >= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the prior value cannot possibly satisfy low_compare, so we can
+		 * give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(array->low_order,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Decrement by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &underflow);
+
+	if (underflow)
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the bounds of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		overflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just copy over array datum (only skip arrays require freeing
+			 * and allocating memory for sk_argument)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element is out of bounds for the array.
+		 *
+		 * This is only possible if high_compare represents an <= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the next value cannot possibly satisfy high_compare, so we can
+		 * give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(array->high_order,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Increment by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &overflow);
+
+	if (overflow)
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the bounds of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1389,6 +2643,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1398,29 +2653,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1480,6 +2736,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1487,7 +2744,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1499,16 +2755,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1633,9 +2883,93 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_NEGPOSINF))
+		{
+			/* Scankey has a valid/comparable sk_argument value (not -inf) */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXT))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument + infinitesimal"
+				 */
+				if (ScanDirectionIsForward(dir))
+				{
+					/* tupdatum is still < "sk_argument + infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was forward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to backward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+			else if (result == 0 && (cur->sk_flags & SK_BT_PRIOR))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument - infinitesimal"
+				 */
+				if (ScanDirectionIsBackward(dir))
+				{
+					/* tupdatum is still > "sk_argument - infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was backward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to forward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+		}
+		else
+		{
+			/*
+			 * Scankey searches for the sentinel value -inf (or +inf).
+			 *
+			 * Note: -inf could mean "true negative infinity", or it could
+			 * just represent the lowest/first value that satisfies the skip
+			 * array's low_compare.  +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * "Compare tupdatum against -inf" using array's low_compare, if
+			 * any (or "compare it against +inf" using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is >= -inf and <= +inf.  It's time for caller to
+				 * advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1967,18 +3301,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -2003,18 +3328,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2032,12 +3348,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -2113,11 +3438,62 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  "Binary searching" only determined whether
+		 * tupdatum is beyond, before, or within the range of the skip array.
+		 *
+		 * As always, we set the array element to its closest available match.
+		 * But unlike with a conventional array, a skip array's new element
+		 * might be NEGPOSINF, which represents the lowest (or the highest)
+		 * real value that is within the range of the skip array.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == "some array element".
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2467,6 +3843,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also cons up skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2479,10 +3857,24 @@ end_toplevel_scan:
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never consed up skip array scan keys, it would be possible for "gaps"
+ * to appear that made it unsafe to mark any subsequent input scan keys (taken
+ * from scan->keyData[]) as required to continue the scan.  If there are no
+ * keys for a given attribute, the keys for subsequent attributes can never be
+ * required.  For instance, "WHERE y = 4" always required a full-index scan
+ * prior to Postgres 18.  Typically, preprocessing is now able to "rewrite"
+ * such a qual into "WHERE x = ANY('{every possible x value}') and y = 4".
+ * The addition of a consed up skip array key on "x" enables marking the key
+ * on "y" (as well as the one on "x") required, according to the usual rules.
+ * Of course, this transformation process cannot change the tuples returned by
+ * the scan -- the skip array will use an IS NULL qual when searching for its
+ * "NULL element" (in this particular example, at least).  But skip arrays can
+ * make the scan much more efficient: if there are few distinct values in "x",
+ * we'll be able to skip over many irrelevant leaf pages.  (If on the other
+ * hand there are many distinct values in "x" then the scan will degenerate
+ * into a full index scan at run time, but we'll be no worse off overall.)
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -2519,7 +3911,12 @@ end_toplevel_scan:
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -2583,6 +3980,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -2669,7 +4074,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			/*
 			 * If = has been specified, all other keys can be eliminated as
 			 * redundant.  If we have a case like key = 1 AND key > 2, we can
-			 * set qual_ok to false and abandon further processing.
+			 * set qual_ok to false and abandon further processing.  Note that
+			 * this is no less true if the = key is SEARCHARRAY; the only real
+			 * difference is that the inequality key _becomes_ redundant by
+			 * making _bt_compare_scankey_args eliminate a subset of array
+			 * elements (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * We also have to deal with the case of "key IS NULL", which is
 			 * unsatisfiable in combination with any other index condition. By
@@ -2722,7 +4131,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -2770,6 +4178,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * Typically, our call to _bt_preprocess_array_keys will have
+			 * already added "=" skip array keys as required to form an
+			 * unbroken series of "=" constraints on all attrs prior to the
+			 * attr from the last scan key that came from our scan->keyData[]
+			 * input scan keys.  However, certain implementation restrictions
+			 * might have prevented array preprocessing from doing that for us
+			 * (e.g., opclasses without an "=" operator can't use skip scan).
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -2858,6 +4274,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -2866,6 +4283,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -2885,8 +4303,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -3028,10 +4444,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3097,6 +4514,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -3106,6 +4526,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3179,6 +4615,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3740,6 +5177,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index db6ed784a..86a5de8f4 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 16144c2b7..2cef9816e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -370,6 +370,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 85e5eaf32..88c929a0a 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -98,6 +98,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 8130f3e8a..256350007 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f73f294b8..cb8470324 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -85,6 +85,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 08fa6774d..42fde7ef2 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -192,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -213,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5736,6 +5740,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6794,6 +6884,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6803,17 +6940,22 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6829,14 +6971,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -6846,13 +6993,90 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't absord a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6874,6 +7098,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6894,7 +7119,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6910,6 +7135,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6925,6 +7182,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -7033,104 +7291,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 5284d23dc..654bda638 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -384,6 +387,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 8a67f0120..6befee3a4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1751,6 +1752,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3590,6 +3602,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index d3358dfc3..fa6f27e6d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1938,7 +1938,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -1958,7 +1958,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 9b2973694..4c4909691 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4440,24 +4440,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -7562,19 +7563,23 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                        QUERY PLAN                         
------------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Nested Loop
    ->  Hash Join
          Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-         ->  Seq Scan on fkest f2
-               Filter: (x100 = 2)
+         ->  Bitmap Heap Scan on fkest f2
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
          ->  Hash
-               ->  Seq Scan on fkest f1
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f1
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
          Index Cond: (x = f1.x)
-(10 rows)
+(14 rows)
 
 alter table fkest add constraint fk
   foreign key (x, x10b, x100) references fkest (x, x10, x100);
@@ -7583,20 +7588,24 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                     QUERY PLAN                      
------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Hash Join
    Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
    ->  Hash Join
          Hash Cond: (f3.x = f2.x)
          ->  Seq Scan on fkest f3
          ->  Hash
-               ->  Seq Scan on fkest f2
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f2
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Hash
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-(11 rows)
+         ->  Bitmap Heap Scan on fkest f1
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
+(15 rows)
 
 rollback;
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 36dc31c16..aef239200 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5240,9 +5240,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index c73631a9a..62eb4239a 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1461,18 +1461,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index fe162cc7c..dea40e013 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -766,7 +766,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -776,7 +776,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1847bbfa9..73264ec9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2664,6 +2665,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

#46Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Peter Geoghegan (#45)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Hi,

Apologies for the delayed response. I've confirmed that the costing is
significantly
improved for multicolumn indexes in the case I provided. Thanks!
/messages/by-id/TYWPR01MB10982A413E0EC4088E78C0E11B1A62@TYWPR01MB10982.jpnprd01.prod.outlook.com

I have some comments and questions regarding the v14 patch.

(1)

v14-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patch

As a user, I agree with adding a counter for skip scans
(though it might be better to reply to the different thread).

I believe this will help users determine whether the skip scan
optimization is
effective. If the ratio of (actual rows)/(index searches) becomes small,
it
suggests that many traversals are occurring, indicating that the skip
scan
optimization is not working efficiently. In such cases, users might
benefit
from replacing the multi-column index with an optimized leading column
index.

IIUC, why not add it to the documentation? It would clearly help users
understand how to tune their queries using the counter, and it would
also show that the counter is not just for developers.

(2)

v14-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patch

From the perspective of consistency, wouldn't it be better to align the
naming
between the EXPLAIN output and pg_stat_all_indexes.idx_scan, even though
the
documentation states they refer to the same concept?

I personally prefer something like "search" instead of "scan", as "scan"
is
commonly associated with node names like Index Scan and similar terms.
To maintain
consistency, how about renaming pg_stat_all_indexes.idx_scan to
pg_stat_all_indexes.idx_search?

(3)

v14-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patch

The counter should be added in blgetbitmap().

(4)

v14-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patch
doc/src/sgml/bloom.sgml

The below forgot "Index Searches: 1".

-&gt; Bitmap Index Scan on btreeidx2 (cost=0.00..12.04
rows=500 width=0) (never executed)
Index Cond: (i2 = 898732)
Planning Time: 0.491 ms
Execution Time: 0.055 ms
(10 rows)

Although we may not need to fix it, due to the support for skip scan,
the B-tree
index is now selected over the Bloom index in my environment.

(5)

v14-0002-Add-skip-scan-to-nbtree.patch

Although I tested with various data types such as int, uuid, oid, and
others on my
local PC, I could only identify the regression case that you already
mentioned.

The case is that Index(Only)Scan is selected instead of SeqScan.
Although the performance
of the full index scan is almost the same as that of SeqScan, the skip
scan degrades by
a factor of 4, despite the number of accessed pages being almost the
same. The flamegraph
indicates that the ratio of _bt_advance_array_keys() and
_bt_tuple_before_array_skeys()
becomes high.

Although it's not an optimal solution and would only reduce the degree
of performance
degradation, how about introducing a threshold per page to switch from
skip scan to full
index scan? For example, switch to full index scan after
_bt_tuple_before_array_skeys() in
_bt_checkkeys() fails 30 times per page. While 30 is just an example, I
referenced the
following case, which seems close to the worst-case scenario. In that
case,
_bt_advance_array_keys() and _bt_tuple_before_array_skeys() are called
almost 333 times per
page (1,000,000 records / 2,636 pages), and 30 is about 1/10 of that
number.

# Example case (see test.sql)
# master: 51.612ms
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..10633.43 rows=1 width=8) (actual
time=0.160..51.612 rows=1 loops=1)
Output: id1, id2
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=4425
-> Parallel Seq Scan on public.t (cost=0.00..9633.33 rows=1
width=8) (actual time=31.728..48.378 rows=0 loops=3)
Output: id1, id2
Filter: (t.id2 = 100)
Rows Removed by Filter: 333333
Buffers: shared hit=4425
Worker 0: actual time=47.583..47.583 rows=0 loops=1
Buffers: shared hit=1415
Worker 1: actual time=47.568..47.569 rows=0 loops=1
Buffers: shared hit=1436
Settings: effective_cache_size = '7932MB', work_mem = '15MB'
Planning Time: 0.078 ms
Execution Time: 51.630 ms
(17 rows)

# master with v14patch: 199.328ms. 4x slower
SET skipscan_prefix_cols=32;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Index Only Scan using t_idx on public.t (cost=0.42..3535.75 rows=1
width=8) (actual time=0.090..199.328 rows=1 loops=1)
Output: id1, id2
Index Cond: (t.id2 = 100)
Index Searches: 1
Heap Fetches: 0
Buffers: shared hit=2736
Settings: effective_cache_size = '7932MB', work_mem = '15MB'
Planning Time: 0.069 ms
Execution Time: 199.348 ms
(9 rows)

# master with v14patch: 54.665ms.
SET skipscan_prefix_cols=0; // disable skip scan
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
Index Only Scan using t_idx on public.t (cost=0.42..3535.75 rows=1
width=8) (actual time=0.031..54.665 rows=1 loops=1)
Output: id1, id2
Index Cond: (t.id2 = 100)
Index Searches: 1
Heap Fetches: 0
Buffers: shared hit=2736
Settings: effective_cache_size = '7932MB', work_mem = '15MB'
Planning Time: 0.067 ms
Execution Time: 54.685 ms
(9 rows)

(6)

v14-0002-Add-skip-scan-to-nbtree.patch
src/backend/access/nbtree/nbtutils.c

+static int
+_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc 
*skipatts,
+							  int *numSkipArrayKeys)
(snip)
+		int			prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)

Is it better to move prev_numSkipArrayKeys =*numSkipArrayKeys after the
while loop?
For example, the index below should return *numSkipArrayKeys = 0 instead
of 1
if the id3 type does not support eq_op.

* index: CREATE INDEX test_idx on TEST (id1 int, id2 int, id3 no_eq_op,
id4 int);
* query: SELECT * FROM test WHERE id4 = 10;

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

Attachments:

test.sqltext/plain; name=test.sqlDownload
In reply to: Masahiro Ikeda (#46)
3 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Hi Masahiro,

On Tue, Nov 19, 2024 at 3:30 AM Masahiro Ikeda <ikedamsh@oss.nttdata.com> wrote:

Apologies for the delayed response. I've confirmed that the costing is
significantly
improved for multicolumn indexes in the case I provided. Thanks!
/messages/by-id/TYWPR01MB10982A413E0EC4088E78C0E11B1A62@TYWPR01MB10982.jpnprd01.prod.outlook.com

Great! I made it one of my private/internal test cases for the
costing. Your test case was quite helpful.

Attached is v15. It works through your feedback.

Importantly, v15 has a new patch which has a fix for your test.sql
case -- which is the most important outstanding problem with the patch
(and has been for a long time now). I've broken those changes out into
a separate patch because they're still experimental, and have some
known minor bugs. But it works well enough for you to assess how close
I am to satisfactorily fixing the known regressions, so it seems worth
posting quickly.

IIUC, why not add it to the documentation? It would clearly help users
understand how to tune their queries using the counter, and it would
also show that the counter is not just for developers.

The documentation definitely needs more work. I have a personal TODO
item about that.

Changes to the documentation can be surprisingly contentious, so I
often work on it last, when we have the clearest picture of how to
talk about the feature. For example, Matthias said something that's
approximately the opposite of what you said about it (though I agree
with you about it).

From the perspective of consistency, wouldn't it be better to align the
naming
between the EXPLAIN output and pg_stat_all_indexes.idx_scan, even though
the
documentation states they refer to the same concept?

I personally prefer something like "search" instead of "scan", as "scan"
is
commonly associated with node names like Index Scan and similar terms.
To maintain
consistency, how about renaming pg_stat_all_indexes.idx_scan to
pg_stat_all_indexes.idx_search?

I suspect that other hackers will reject that proposal on
compatibility grounds, even though it would make sense in a "green
field" situation.

Honestly, discussions about UI/UX details such as EXPLAIN ANALYZE
always tend to result in unproductive bikeshedding. What I really want
is something that will be acceptable to all parties. I don't have any
strong opinions of my own about it -- I just think that it's important
to show *something* like "Index Searches: N" to make skip scan user
friendly.

(3)

v14-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patch

The counter should be added in blgetbitmap().

Fixed.

(4)

v14-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patch
doc/src/sgml/bloom.sgml

The below forgot "Index Searches: 1".

-&gt; Bitmap Index Scan on btreeidx2 (cost=0.00..12.04
rows=500 width=0) (never executed)
Index Cond: (i2 = 898732)
Planning Time: 0.491 ms
Execution Time: 0.055 ms
(10 rows)

Fixed (though I made it show "Index Searches: 0" instead, since this
particular index scan node is "never executed").

Although we may not need to fix it, due to the support for skip scan,
the B-tree
index is now selected over the Bloom index in my environment.

I am not inclined to change it.

Although I tested with various data types such as int, uuid, oid, and
others on my
local PC, I could only identify the regression case that you already
mentioned.

That's good news!

Although it's not an optimal solution and would only reduce the degree
of performance
degradation, how about introducing a threshold per page to switch from
skip scan to full
index scan?

The approach to fixing these regressions from the new experimental
patch doesn't need to use any such threshold. It is effective both
with simple "WHERE id2 = 100" cases (like the queries from your
test.sql test case), as well as more complicated "WHERE id2 BETWEEN 99
AND 101" inequality cases.

What do you think? The regressions are easily under 5% with the new
patch applied, which is in the noise.

At the same time, we're just as capable of skipping whenever the scan
encounters a large group of skipped-prefix-column duplicates. For
example, if I take your test.sql test case and add another insert that
adds such a group (e.g., "INSERT INTO t SELECT 55, i FROM
generate_series(-1000000, 1000000) i;" ), and then re-run the query,
the scan is exactly as fast as before -- it just skips to get over the
newly inserted "55" group of tuples. Obviously, this also makes the
master branch far, far slower.

As I've said many times already, the need to be flexible and offer
robust performance in cases where skipping is either very effective or
very ineffective *during the same index scan* seems very important to
me. This "55" variant of your test.sql test case is a great example of
the kinds of cases I was thinking about.

Is it better to move prev_numSkipArrayKeys =*numSkipArrayKeys after the
while loop?
For example, the index below should return *numSkipArrayKeys = 0 instead
of 1
if the id3 type does not support eq_op.

* index: CREATE INDEX test_idx on TEST (id1 int, id2 int, id3 no_eq_op,
id4 int);
* query: SELECT * FROM test WHERE id4 = 10;

Nice catch! You're right. Fixed this in v15, too.

Thanks for the review
--
Peter Geoghegan

Attachments:

v15-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v15-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From 6dba6e5a0245196c05ab19d840d62a4c925adebb Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v15 1/3] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 contrib/bloom/blscan.c                        |   1 +
 doc/src/sgml/bloom.sgml                       |   6 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 22 files changed, 243 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index e1884acf4..7b4180db5 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -153,6 +153,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index c0b978119..52939d317 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -585,6 +585,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index f2fd62afb..5e423e155 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index b35b8a975..36f1435cb 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index 0d99d6abc..927ba1039 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 60c61039d..ec259012b 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index dd76fe1da..8bbb3d734 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -69,6 +69,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -552,6 +553,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -578,6 +580,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -705,6 +708,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			Assert(btscan->btps_nextScanPage != P_NONE);
 			*next_scan_page = btscan->btps_nextScanPage;
@@ -805,6 +813,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -839,6 +849,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 0cd046613..82bb93d1a 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -970,6 +970,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 301786185..be668abf2 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7c0fd63b2..6cb5ebcd2 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2098,6 +2100,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2111,6 +2114,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2127,6 +2131,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2645,6 +2650,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index 0c5fb725e..f77f716f0 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -116,6 +116,7 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++)
 	{
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 19f2b172c..92b13f539 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -170,9 +170,10 @@ CREATE INDEX
    Heap Blocks: exact=28
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..1792.00 rows=2 width=0) (actual time=0.356..0.356 rows=29 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 0.408 ms
-(8 rows)
+(9 rows)
 </programlisting>
   </para>
 
@@ -202,11 +203,12 @@ CREATE INDEX
    -&gt;  BitmapAnd  (cost=24.34..24.34 rows=2 width=0) (actual time=0.027..0.027 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..12.04 rows=500 width=0) (actual time=0.026..0.026 rows=0 loops=1)
                Index Cond: (i5 = 123451)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
-(9 rows)
+(10 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 840d7f816..c68eb770e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4198,12 +4198,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index cd12b9ce4..482715397 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -727,8 +727,10 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Heap Blocks: exact=10
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 1
  Planning Time: 0.485 ms
  Execution Time: 0.073 ms
 </screen>
@@ -779,6 +781,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Heap Blocks: exact=90
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
  Planning Time: 0.187 ms
  Execution Time: 3.036 ms
 </screen>
@@ -844,6 +847,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -873,9 +877,11 @@ EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique
          Buffers: shared hit=4 read=3
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared hit=2
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
                Buffers: shared hit=2 read=3
  Planning:
    Buffers: shared hit=3
@@ -908,6 +914,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Heap Blocks: exact=90
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1042,6 +1049,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
  Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
  Planning Time: 0.077 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a854..e042638b7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -502,9 +502,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    Batches: 1  Memory Usage: 24kB
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(7 rows)
+(8 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index ae9ce9d8e..c24d56007 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index f6b8329cd..a8bc0bfd7 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7a03b4e36..e3e99272a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2657,47 +2661,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off)
@@ -2713,16 +2726,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2741,7 +2757,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off)
@@ -2757,16 +2773,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2787,7 +2806,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2877,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2897,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,17 +2986,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2982,17 +3013,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3064,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3048,17 +3091,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3112,17 +3161,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3144,17 +3199,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3482,12 +3543,14 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(15);
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3566,10 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(25);
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3617,17 @@ explain (analyze, costs off, summary off, timing off) select * from ma_test wher
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4197,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 33a6dceb0..b7cf35b9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index 2eaeb1477..9afe205e0 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 442428d93..085e746af 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

v15-0003-POC-fix-for-regressions-in-unsympathetic-cases.patchapplication/octet-stream; name=v15-0003-POC-fix-for-regressions-in-unsympathetic-cases.patchDownload
From 3d12aeba061337c5e26da6551c0a93da15334476 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v15 3/3] POC fix for regressions in unsympathetic cases.

Fix regressions in cases that are nominally eligible to use skip scan
but can never actually benefit from skipping.  These are cases where the
leading skipped prefix column contains many distinct values -- often as
many distinct values are there are total index tuples.

For example, the test case posted here is fixed by the work from this
commit:

https://postgr.es/m/51d00219180323d121572e1f83ccde2a@oss.nttdata.com

Note that this commit doesn't actually change anything about when or how
skip scan decides when or how to skip.  It just avoids wasting CPU
cycles on uselessly maintaining a skip array at the tuple granularity,
preferring to maintain the skip arrays at something closer to the page
granularity when that makes sense.  See:

https://www.postgresql.org/message-id/flat/CAH2-Wz%3DE7XrkvscBN0U6V81NK3Q-dQOmivvbEsjG-zwEfDdFpg%40mail.gmail.com#7d34e8aa875d7a718043834c5ef4c167

Doing well on cases like this is important because we can't expect the
optimizer to never choose an affected plan -- we prefer to solve these
problems in the executor, which has access to the most reliable and
current information about the index.  The optimizer can afford to be
very optimistic about skipping if actual runtime scan behavior is very
similar to a traditional full index scan in the worst case.  See
"optimizer" section from the original intro mail for more information:

https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP%2BG4bw%40mail.gmail.com
---
 src/include/access/nbtree.h           |  5 +-
 src/backend/access/nbtree/nbtsearch.c |  5 ++
 src/backend/access/nbtree/nbtutils.c  | 70 ++++++++++++++++++---------
 3 files changed, 54 insertions(+), 26 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index d841e85bc..06ccab8cf 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1112,11 +1112,12 @@ typedef struct BTReadPageState
 	bool		firstmatch;		/* at least one match so far?  */
 
 	/*
-	 * Private _bt_checkkeys state used to manage "look ahead" optimization
-	 * (only used during scans with array keys)
+	 * Private _bt_checkkeys state used to manage "look ahead" and skip array
+	 * optimizations (only used during scans with array keys)
 	 */
 	int16		rechecks;
 	int16		targetdistance;
+	bool		beyondskip;
 
 } BTReadPageState;
 
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 43e321896..578bafbf4 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1646,6 +1646,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.firstmatch = false;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
+	pstate.beyondskip = false;
 
 	/*
 	 * Prechecking the value of the continuescan flag for the last item on the
@@ -1837,6 +1838,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
+			pstate.beyondskip = false; /* reset for finaltup */
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -1893,6 +1895,9 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			else
 				tuple_alive = true;
 
+			if (offnum == minoff)
+				pstate.beyondskip = false; /* reset for finaltup */
+
 			itup = (IndexTuple) PageGetItem(page, iid);
 			Assert(!BTreeTupleIsPivot(itup));
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 5117fb164..a3b59789a 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -151,7 +151,8 @@ static bool _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption);
 static void _bt_mark_scankey_required(ScanKey skey);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool beyondskip,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
@@ -3136,6 +3137,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	int			arrayidx = 0;
 	bool		beyond_end_advance = false,
 				has_required_opposite_direction_only = false,
+				has_required_opposite_direction_skip = false,
 				oppodir_inequality_sktrig = false,
 				all_required_satisfied = true,
 				all_satisfied = true;
@@ -3189,6 +3191,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			{
 				array = &so->arrayKeys[arrayidx++];
 				Assert(array->scan_key == ikey);
+				if (array->num_elems == -1 && ScanDirectionIsForward(dir) ?
+					array->low_compare :
+					array->high_compare)
+					has_required_opposite_direction_skip = true;
 			}
 		}
 		else
@@ -3209,10 +3215,8 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		if (ikey < sktrig)
 			continue;
 
-		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
+		if (sktrig_required && (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -3416,7 +3420,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (required || (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 				all_required_satisfied = false;
 			else
 			{
@@ -3501,6 +3505,12 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 	Assert(_bt_verify_keys_with_arraykeys(scan));
 
+	if (beyond_end_advance && pstate->finaltup &&
+		!has_required_opposite_direction_skip)
+		pstate->beyondskip = true;
+	else if (pstate)
+		pstate->beyondskip = false;
+
 	/*
 	 * Does tuple now satisfy our new qual?  Recheck with _bt_check_compare.
 	 *
@@ -3528,7 +3538,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, false, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -3556,11 +3566,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * that we can know to be safe based on caller's tuple alone.  If we
 		 * didn't perform this step, then that guarantee wouldn't quite hold.
 		 */
-		if (unlikely(!continuescan))
+		if (unlikely(!continuescan) && sktrig_required)
 		{
 			bool		satisfied PG_USED_FOR_ASSERTS_ONLY;
 
-			Assert(sktrig_required);
 			Assert(so->keyData[nsktrig].sk_strategy != BTEqualStrategyNumber);
 
 			/*
@@ -3615,9 +3624,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * for _bt_check_compare to behave as if they are required in the current
 	 * scan direction to deal with NULLs.  We'll account for that separately.)
 	 */
-	Assert(_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts,
-										false, 0, NULL) ==
-		   !all_required_satisfied);
+	/* FIXME Add back the assertion removed from here */
 
 	/*
 	 * We generally permit primitive index scans to continue onto the next
@@ -4904,9 +4911,9 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 
-	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
-							&pstate->continuescan, &ikey);
+	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, arrayKeys,
+							pstate->beyondskip, pstate->prechecked,
+							pstate->firstmatch, &pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
 	if (!arrayKeys && so->numArrayKeys)
@@ -4917,11 +4924,11 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
 		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
-		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
-											 tupnatts, false, 0, NULL));
+		Assert(!pstate->beyondskip && !pstate->prechecked &&
+			   !pstate->firstmatch);
+		/* FIXME Add back the assertion removed from here */
 	}
-	if (pstate->prechecked || pstate->firstmatch)
+	if (!pstate->beyondskip && (pstate->prechecked || pstate->firstmatch))
 	{
 		bool		dcontinuescan;
 		int			dikey = 0;
@@ -4931,7 +4938,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, false, false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -5062,7 +5069,7 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	Assert(so->numArrayKeys);
 
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -5107,11 +5114,17 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Callers with skip arrays can pass beyondskip=true to have us assume that
+ * the scan is still likely to be before the current array keys according to
+ * _bt_tuple_before_array_skeys.  We can safely avoid evaluating skip array
+ * scan keys when this happens.
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool beyondskip,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -5128,10 +5141,19 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction -- though
+		 * only when "beyond advancement" skip array optimization isn't in use
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (beyondskip &&
+			!(key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)))
+		{
+			if (key->sk_flags & SK_BT_SKIP)
+				continue;
+		}
+		else if (((key->sk_flags & SK_BT_REQFWD) &&
+				  ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) &&
+				  ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
-- 
2.45.2

v15-0002-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v15-0002-Add-skip-scan-to-nbtree.patchDownload
From f27cce981c0d4a0b4a5e9e5a8f76b1b4f5b7a171 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v15 2/3] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
useful in cases where the total number of distinct values in the column
'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.

The design of skip arrays allows array advancement to work without
concern for which specific array types are involved; only the lowest
level code needs to treat skip arrays and SAOP arrays differently.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values are conceptually just like any
other value that might appear in either kind of array.  A call made to
_bt_first to reposition the scan based on a NEXT/PRIOR sentinel usually
won't locate a leaf page containing tuples that must be returned to the
scan, but that's normal during any skip scan that happens to involve
"sparse" indexed values (skip support can only help with "dense" indexed
values, which isn't meaningfully possible when the skipped column is of
a continuous type, where skip support typically can't be implemented).

The optimizer doesn't use distinct new index paths to represent index
skip scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  Skipping is possible during
eligible bitmap index scans, index scans, and index-only scans.  It's
also possible during eligible parallel scans.  An eligible scan is any
scan that nbtree preprocessing generates a skip array for, which happens
whenever doing so will enable later preprocessing to mark original input
scan keys (passed by the executor) as required to continue the scan.

There are hardly any limitations around where skip arrays/scan keys may
appear relative to conventional/input scan keys.  This is no less true
in the presence of conventional SAOP array scan keys, which may both
roll over and be rolled over by skip arrays.  For example, a skip array
on the column "b" is generated for "WHERE a = 42 AND c IN (5, 6, 7, 8)".
As always (since commit 5bf748b8), whether or not nbtree actually skips
depends in large part on physical index characteristics at runtime.
Preprocessing is optimistic about skipping working out: it applies
simple static rules to decide where to place skip arrays.  It's up to
array-related scan code to keep the overhead of maintaining the scan's
skip arrays under control when it turns out that skipping isn't helpful.

Preprocessing will never cons up a skip array for an index column that
has an equality strategy scan key on input, but will do so for indexed
columns that are only constrained by inequality strategy scan keys.
This allows the scan to skip using a range skip array.  These are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   28 +-
 src/include/catalog/pg_amproc.dat             |   22 +
 src/include/catalog/pg_proc.dat               |   27 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  101 +
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  273 +++
 src/backend/access/nbtree/nbtree.c            |  213 ++-
 src/backend/access/nbtree/nbtsearch.c         |   75 +-
 src/backend/access/nbtree/nbtutils.c          | 1698 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   46 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  379 +++-
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/timestamp.c             |   48 +
 src/backend/utils/adt/uuid.c                  |   71 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/bloom.sgml                       |    1 +
 doc/src/sgml/btree.sgml                       |   34 +-
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   49 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |   10 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   61 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    5 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 36 files changed, 2979 insertions(+), 333 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c51de742e..0826c124f 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -202,7 +202,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 123fba624..d841e85bc 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1022,10 +1024,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* skip scan support (unless !use_sksup) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo   *low_order;		/* low_compare's ORDER proc */
+	FmgrInfo   *high_order;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1113,6 +1127,10 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on omitted prefix column */
+#define SK_BT_NEGPOSINF	0x00080000	/* -inf/+inf key (invalid sk_argument) */
+#define SK_BT_NEXT		0x00100000	/* positions scan > sk_argument */
+#define SK_BT_PRIOR		0x00200000	/* positions scan < sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1149,6 +1167,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1159,7 +1181,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5d7fe292b..c5af25806 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cbbe8acd3..abfe92666 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2260,6 +2275,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4447,6 +4465,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6290,6 +6311,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9327,6 +9351,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d70e6d37e..5b58739ad 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..d97e6059d
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,101 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining pairs of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code falls back on next-key sentinel values for any opclass that
+ * doesn't provide its own skip support function.  There is no benefit to
+ * providing skip support for an opclass on a type where guessing that the
+ * next indexed value is the next possible indexable value seldom works out.
+ * It might not even be feasible to add skip support for a continuous type.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order).
+	 * This gives the B-Tree code a useful value to start from, before any
+	 * data has been read from the index.  These are also sometimes used to
+	 * detect when the scan has run out of indexable values to search for.
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 *
+	 * Operator class decrement/increment functions never have to directly
+	 * deal with the value NULL, nor with ASC vs. DESC column ordering.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 1859be614..b4f1466d4 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..26ebbf776 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 8bbb3d734..105bcfe2f 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -29,6 +29,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -70,7 +71,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -78,11 +79,23 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -535,10 +548,159 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume all input scankeys will be output with its own
+	 * SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * scan array (and associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		Form_pg_attribute attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array/skip scan key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array/skip scan key, while freeing memory allocated
+		 * for old sk_argument where required
+		 */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -549,7 +711,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -572,16 +735,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -608,6 +771,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -652,7 +816,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -674,14 +838,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -719,7 +878,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -763,11 +922,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -806,7 +965,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -815,7 +974,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -833,6 +992,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -842,7 +1002,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -852,14 +1012,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 82bb93d1a..43e321896 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -984,6 +984,13 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  Skip
+	 * array keys are = keys, though we'll sometimes need to treat the key as
+	 * if it was some kind of inequality instead.  This is required whenever
+	 * the skip array's current array element is a sentinel value.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1059,8 +1066,39 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is -inf or +inf, choose low_compare/high_compare
+				 * (or consider if they imply a NOT NULL constraint).
+				 */
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skiparraysk = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					if (!array->null_elem)
+						impliesNN = skiparraysk;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1097,6 +1135,39 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					strat_total == BTLessStrategyNumber)
 					break;
 
+				/*
+				 * If this is a scan key for a skip array whose current
+				 * element is marked NEXT or PRIOR, adjust strat_total
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(strat_total == BTEqualStrategyNumber);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[].
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).
+					 */
+					break;
+				}
+
 				/*
 				 * Done if that was the last attribute, or if next key is not
 				 * in sequence (implying no boundary key is available for the
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index d76032502..5117fb164 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	Oid			eq_op;			/* = op to be used to add array, if any */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -63,16 +91,46 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
+static int	_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+										  int *numSkipArrayKeys);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_decrement(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_increment(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -256,6 +314,12 @@ _bt_freestack(BTStack stack)
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -271,10 +335,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -282,30 +348,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_preprocess_num_array_keys(scan, skipatts,
+												 &numSkipArrayKeys);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys described within skipatts[]
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -320,17 +380,18 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/* Process input keys, and emit skip array keys described by skipatts[] */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -346,19 +407,97 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and its scan key where indicated */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* attribute already had an "=" key on input */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[numArrayKeyData];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].low_order = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].high_order = NULL;	/* for now */
+
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how this
+			 * constructed "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			numArrayKeyData++;	/* keep this scan key/array */
+		}
+
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
+		cur = &arrayKeyData[numArrayKeyData];
 		*cur = scan->keyData[input_ikey];
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -414,7 +553,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -425,7 +564,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -442,7 +581,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -517,23 +656,234 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].low_order = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].high_order = NULL;	/* unused */
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
 	return arrayKeyData;
 }
 
+/*
+ *	_bt_preprocess_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit all required so->arrayKeys[] entries.
+ *
+ * Also sets *numSkipArrayKeys to the number of skip arrays that caller must
+ * make sure to add to the scan keys it'll output (complete with corresponding
+ * so->arrayKeys[] entries), and sets the corresponding attributes positions
+ * in *skipatts argument.  Every attribute that receives a skip array will
+ * have its skip support routine set in *skipatts, too.
+ */
+static int
+_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+							  int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_inkey = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				(*numSkipArrayKeys)++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				break;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatt
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatt->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										 BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack even an equality strategy
+	 * operator, but they're supported.  We cope by making caller not use a
+	 * skip array for this index column (nor for any lower-order columns).
+	 */
+	if (!OidIsValid(skipatt->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatt->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+													   reverse,
+													   &skipatt->sksup);
+
+	/* might not have set up skip support, but can skip either way */
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys_final() -- fix up array scan key references
  *
@@ -633,7 +983,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -669,6 +1020,14 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && !array->null_elem)
+						_bt_skip_preproc_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
@@ -684,7 +1043,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -980,26 +1339,28 @@ _bt_merge_arrays(IndexScanDesc scan, ScanKey skey, FmgrInfo *sortproc,
  * guaranteeing that at least the scalar scan key can be considered redundant.
  * We return false if the comparison could not be made (caller must keep both
  * scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar inequality scan keys.
  */
 static bool
 _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
 	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
 		   skey->sk_strategy != BTEqualStrategyNumber);
@@ -1009,8 +1370,7 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1041,11 +1401,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensures that the scalar scan key can be eliminated as redundant
+		 */
+		eliminated = true;
+
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+	}
+	else
+	{
+		/*
+		 * With a skip array, it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when there's a lack of suitable cross-type support for
+		 * comparing skey to an existing inequality associated with the array.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when
+ * determining its redundancy against a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1097,10 +1511,298 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of a skip array scan key when determining its
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array.  This forces the array's elements to be generated within the limits
+ * of a range that's described/constrained by the array's inequalities.
+ *
+ * Return value indicates if the scalar scan key could be "eliminated".  We
+ * return true in the common case where caller's scan key was successfully
+ * rolled into the skip array.  We return false when we can't do that due to
+ * the presence of an existing, conflicting inequality that cannot be compared
+ * to caller's inequality due to a lack of suitable cross-type support.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way scan code can generally assume that
+	 * it'll always be safe to use the ORDER procs stored in so->orderProcs[].
+	 * The only exception is cases where the array's scan key is NEGPOSINF.
+	 *
+	 * When the array's scan key is marked NEGPOSINF, then it'll lack a valid
+	 * sk_argument.  The scan must apply the array's low_compare and/or
+	 * high_compare (if any) to establish whether a given tupdatum is within
+	 * the range of the skip array.  This could involve applying the 3-way
+	 * low_order/high_order ORDER proc we store here (though never the ORDER
+	 * proc stored in so->orderProcs[]).
+	 *
+	 * Note: we sometimes use low_compare/high_compare inequalities with the
+	 * array's current element value, and with values that were mutated by the
+	 * array's skip support routine.  A skip array must always use its index
+	 * attribute's own input opclass/type, so this will always work correctly.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (!array->high_compare)
+			{
+				/* array currently lacks a high_compare, make space for one */
+				array->high_compare = palloc(sizeof(ScanKeyData));
+				array->high_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare, keep old one */
+
+				/* replace old high_compare with caller's new one */
+			}
+
+			/* caller's high_compare becomes skip array's high_compare */
+			*array->high_compare = *skey;
+			*array->high_order = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (!array->low_compare)
+			{
+				/* array currently lacks a low_compare, make space for one */
+				array->low_compare = palloc(sizeof(ScanKeyData));
+				array->low_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare, keep old one */
+
+				/* replace old low_compare with caller's new one */
+			}
+
+			/* caller's low_compare becomes skip array's low_compare */
+			*array->low_compare = *skey;
+			*array->low_order = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
 
+/*
+ * Perform final preprocessing for a skip array.  Called when the skip array's
+ * final low_compare and high_compare have been chosen.
+ *
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts the
+ * operator strategies).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value NEGPOSINF, using >= instead of using >
+ * (or using <= instead of < during backwards scans) makes it safe to include
+ * lower-order scan keys in the insertion scan key (there must be lower-order
+ * scan keys after the skip array).  We will avoid an extra _bt_first to find
+ * the first value in the index > sk_argument, at least when the first real
+ * matching value in the index happens to be an exact match for the newly
+ * transformed (newly incremented/decremented) sk_argument value.
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01' -- which would be wrong.
+ */
+static void
+_bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+	Assert(!array->null_elem);
+
+	/* If skip array doesn't have skip support, can't apply optimization */
+	if (!array->use_sksup)
+		return;
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skip_preproc_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skip_preproc_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skip_preproc_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+	Assert(array->use_sksup);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type.
+	 *
+	 * Note: we have to support the convention that sk_subtype == InvalidOid
+	 * means the opclass input type.
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup.decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skip_preproc_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+	Assert(array->use_sksup);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type.
+	 *
+	 * Note: we have to support the convention that sk_subtype == InvalidOid
+	 * means the opclass input type.
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup.increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  * qsort_arg comparator for sorting array elements
  */
@@ -1220,6 +1922,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1342,6 +2046,98 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, because the array doesn't really
+ * have any elements (it generates its array elements procedurally instead).
+ * Note that this may include a NULL value/an IS NULL qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ *
+ * Note: This function doesn't actually binary search.  It uses an interface
+ * that's as similar to _bt_binsrch_array_skey as possible for consistency.
+ * "Binary searching a skip array" just means determining if tupdatum/tupnull
+ * are before, within, or beyond the range of caller's skip array.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1351,29 +2147,480 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting skip array to lowest element, which isn't just NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Setting skip array to highest element, which isn't just NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element is out of bounds for the array.
+		 *
+		 * This is only possible if low_compare represents an >= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the prior value cannot possibly satisfy low_compare, so we can
+		 * give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(array->low_order,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Decrement by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &uflow);
+	if (uflow)
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the bounds of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element is out of bounds for the array.
+		 *
+		 * This is only possible if high_compare represents an <= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the next value cannot possibly satisfy high_compare, so we can
+		 * give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(array->high_order,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Increment by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &oflow);
+	if (oflow)
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the bounds of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1389,6 +2636,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1398,29 +2646,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1480,6 +2729,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1487,7 +2737,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1499,16 +2748,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1633,9 +2876,93 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_NEGPOSINF))
+		{
+			/* Scankey has a valid/comparable sk_argument value (not -inf) */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXT))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument + infinitesimal"
+				 */
+				if (ScanDirectionIsForward(dir))
+				{
+					/* tupdatum is still < "sk_argument + infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was forward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to backward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+			else if (result == 0 && (cur->sk_flags & SK_BT_PRIOR))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument - infinitesimal"
+				 */
+				if (ScanDirectionIsBackward(dir))
+				{
+					/* tupdatum is still > "sk_argument - infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was backward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to forward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+		}
+		else
+		{
+			/*
+			 * Scankey searches for the sentinel value -inf (or +inf).
+			 *
+			 * Note: -inf could mean "true negative infinity", or it could
+			 * just represent the lowest/first value that satisfies the skip
+			 * array's low_compare.  +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * "Compare tupdatum against -inf" using array's low_compare, if
+			 * any (or "compare it against +inf" using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is >= -inf and <= +inf.  It's time for caller to
+				 * advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1967,18 +3294,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -2003,18 +3321,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2032,12 +3341,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -2113,11 +3431,62 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  "Binary searching" only determined whether
+		 * tupdatum is beyond, before, or within the range of the skip array.
+		 *
+		 * As always, we set the array element to its closest available match.
+		 * But unlike with a conventional array, a skip array's new element
+		 * might be NEGPOSINF, which represents the lowest (or the highest)
+		 * real value that is within the range of the skip array.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == "some array element".
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2467,6 +3836,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2479,10 +3850,24 @@ end_toplevel_scan:
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never consed up skip array scan keys, it would be possible for "gaps"
+ * to appear that made it unsafe to mark any subsequent input scan keys (taken
+ * from scan->keyData[]) as required to continue the scan.  If there are no
+ * keys for a given attribute, the keys for subsequent attributes can never be
+ * required.  For instance, "WHERE y = 4" always required a full-index scan
+ * prior to Postgres 18.  Typically, preprocessing is now able to "rewrite"
+ * such a qual into "WHERE x = ANY('{every possible x value}') and y = 4".
+ * The addition of a consed up skip array key on "x" enables marking the key
+ * on "y" (as well as the one on "x") required, according to the usual rules.
+ * Of course, this transformation process cannot change the tuples returned by
+ * the scan -- the skip array will use an IS NULL qual when searching for its
+ * "NULL element" (in this particular example, at least).  But skip arrays can
+ * make the scan much more efficient: if there are few distinct values in "x",
+ * we'll be able to skip over many irrelevant leaf pages.  (If on the other
+ * hand there are many distinct values in "x" then the scan will degenerate
+ * into a full index scan at run time, but we'll be no worse off overall.)
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -2519,7 +3904,12 @@ end_toplevel_scan:
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -2583,6 +3973,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -2669,7 +4067,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			/*
 			 * If = has been specified, all other keys can be eliminated as
 			 * redundant.  If we have a case like key = 1 AND key > 2, we can
-			 * set qual_ok to false and abandon further processing.
+			 * set qual_ok to false and abandon further processing.  Note that
+			 * this is no less true if the = key is SEARCHARRAY; the only real
+			 * difference is that the inequality key _becomes_ redundant by
+			 * making _bt_compare_scankey_args eliminate a subset of array
+			 * elements (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * We also have to deal with the case of "key IS NULL", which is
 			 * unsatisfiable in combination with any other index condition. By
@@ -2722,7 +4124,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -2770,6 +4171,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * Typically, our call to _bt_preprocess_array_keys will have
+			 * already added "=" skip array keys as required to form an
+			 * unbroken series of "=" constraints on all attrs prior to the
+			 * attr from the last scan key that came from our scan->keyData[]
+			 * input scan keys.  However, certain implementation restrictions
+			 * might have prevented array preprocessing from doing that for us
+			 * (e.g., opclasses without an "=" operator can't use skip scan).
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -2858,6 +4267,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -2866,6 +4276,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -2885,8 +4296,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -3028,10 +4437,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3097,6 +4507,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -3106,6 +4519,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3179,6 +4608,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3740,6 +5170,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index db6ed784a..86a5de8f4 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 16144c2b7..2cef9816e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -370,6 +370,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 85e5eaf32..88c929a0a 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -98,6 +98,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 8130f3e8a..256350007 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f73f294b8..cb8470324 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -85,6 +85,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 08fa6774d..42fde7ef2 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -192,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -213,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5736,6 +5740,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6794,6 +6884,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6803,17 +6940,22 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6829,14 +6971,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -6846,13 +6993,90 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't absord a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6874,6 +7098,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6894,7 +7119,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6910,6 +7135,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6925,6 +7182,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -7033,104 +7291,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index e00cd3c96..213a6326f 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2286,6 +2287,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 5284d23dc..654bda638 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -384,6 +387,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 8a67f0120..6befee3a4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1751,6 +1752,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3590,6 +3602,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 92b13f539..6fa688769 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -206,6 +206,7 @@ CREATE INDEX
                Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
+               Index Searches: 0
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
 (10 rows)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index d3358dfc3..fa6f27e6d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1938,7 +1938,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -1958,7 +1958,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 9b2973694..4c4909691 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4440,24 +4440,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -7562,19 +7563,23 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                        QUERY PLAN                         
------------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Nested Loop
    ->  Hash Join
          Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-         ->  Seq Scan on fkest f2
-               Filter: (x100 = 2)
+         ->  Bitmap Heap Scan on fkest f2
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
          ->  Hash
-               ->  Seq Scan on fkest f1
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f1
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
          Index Cond: (x = f1.x)
-(10 rows)
+(14 rows)
 
 alter table fkest add constraint fk
   foreign key (x, x10b, x100) references fkest (x, x10, x100);
@@ -7583,20 +7588,24 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                     QUERY PLAN                      
------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Hash Join
    Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
    ->  Hash Join
          Hash Cond: (f3.x = f2.x)
          ->  Seq Scan on fkest f3
          ->  Hash
-               ->  Seq Scan on fkest f2
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f2
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Hash
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-(11 rows)
+         ->  Bitmap Heap Scan on fkest f1
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
+(15 rows)
 
 rollback;
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 36dc31c16..aef239200 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5240,9 +5240,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index c73631a9a..62eb4239a 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1461,18 +1461,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index fe162cc7c..dea40e013 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -766,7 +766,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -776,7 +776,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 08521d51a..89f021ccd 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2666,6 +2667,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

#48Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Peter Geoghegan (#47)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Hi Peter,

On 2024-11-20 04:06, Peter Geoghegan wrote:

Hi Masahiro,

On Tue, Nov 19, 2024 at 3:30 AM Masahiro Ikeda
<ikedamsh@oss.nttdata.com> wrote:

Apologies for the delayed response. I've confirmed that the costing is
significantly
improved for multicolumn indexes in the case I provided. Thanks!
/messages/by-id/TYWPR01MB10982A413E0EC4088E78C0E11B1A62@TYWPR01MB10982.jpnprd01.prod.outlook.com

Great! I made it one of my private/internal test cases for the
costing. Your test case was quite helpful.

Attached is v15. It works through your feedback.

Importantly, v15 has a new patch which has a fix for your test.sql
case -- which is the most important outstanding problem with the patch
(and has been for a long time now). I've broken those changes out into
a separate patch because they're still experimental, and have some
known minor bugs. But it works well enough for you to assess how close
I am to satisfactorily fixing the known regressions, so it seems worth
posting quickly.

Thanks for your quick response!

IIUC, why not add it to the documentation? It would clearly help users
understand how to tune their queries using the counter, and it would
also show that the counter is not just for developers.

The documentation definitely needs more work. I have a personal TODO
item about that.

Changes to the documentation can be surprisingly contentious, so I
often work on it last, when we have the clearest picture of how to
talk about the feature. For example, Matthias said something that's
approximately the opposite of what you said about it (though I agree
with you about it).

OK, I understood.

From the perspective of consistency, wouldn't it be better to align
the
naming
between the EXPLAIN output and pg_stat_all_indexes.idx_scan, even
though
the
documentation states they refer to the same concept?

I personally prefer something like "search" instead of "scan", as
"scan"
is
commonly associated with node names like Index Scan and similar terms.
To maintain
consistency, how about renaming pg_stat_all_indexes.idx_scan to
pg_stat_all_indexes.idx_search?

I suspect that other hackers will reject that proposal on
compatibility grounds, even though it would make sense in a "green
field" situation.

Honestly, discussions about UI/UX details such as EXPLAIN ANALYZE
always tend to result in unproductive bikeshedding. What I really want
is something that will be acceptable to all parties. I don't have any
strong opinions of my own about it -- I just think that it's important
to show *something* like "Index Searches: N" to make skip scan user
friendly.

OK, I agree.

Although it's not an optimal solution and would only reduce the degree
of performance
degradation, how about introducing a threshold per page to switch from
skip scan to full
index scan?

The approach to fixing these regressions from the new experimental
patch doesn't need to use any such threshold. It is effective both
with simple "WHERE id2 = 100" cases (like the queries from your
test.sql test case), as well as more complicated "WHERE id2 BETWEEN 99
AND 101" inequality cases.

What do you think? The regressions are easily under 5% with the new
patch applied, which is in the noise.

I didn't come up with the idea. At first glance, your idea seems good
for all cases.

Actually, test.sql shows a performance improvement, and the performance
is almost the same as the master's seqscan. To be precise, the master's
performance is 10-20% better than the v15 patch because the seqscan is
executed in parallel. However, the v15 patch is twice as fast as when
seqscan is not executed in parallel.

However, I found that there is still a problematic case when I read your
patch. IIUC, beyondskip becomes true only if the tuple's id2 is greater
than the scan key value. Therefore, the following query (see
test_for_v15.sql)
still degrades.

-- build without '--enable-debug' '--enable-cassert' 'CFLAGS=-O0 '
-- SET skipscan_prefix_cols=32;
Index Only Scan using t_idx on public.t (cost=0.42..3535.75 rows=1
width=8) (actual time=65.767..65.770 rows=1 loops=1)
Output: id1, id2
Index Cond: (t.id2 = 1000000)
Index Searches: 1
Heap Fetches: 0
Buffers: shared hit=2736
Settings: effective_cache_size = '7932MB', work_mem = '15MB'
Planning Time: 0.058 ms
Execution Time: 65.782 ms
(9 rows)

-- SET skipscan_prefix_cols=0;
Index Only Scan using t_idx on public.t (cost=0.42..3535.75 rows=1
width=8) (actual time=17.276..17.278 rows=1 loops=1)
Output: id1, id2
Index Cond: (t.id2 = 1000000)
Index Searches: 1
Heap Fetches: 0
Buffers: shared hit=2736
Settings: effective_cache_size = '7932MB', work_mem = '15MB'
Planning Time: 0.044 ms
Execution Time: 17.290 ms
(9 rows)

I’m reporting the above result, though you might already be aware of the
issue.

At the same time, we're just as capable of skipping whenever the scan
encounters a large group of skipped-prefix-column duplicates. For
example, if I take your test.sql test case and add another insert that
adds such a group (e.g., "INSERT INTO t SELECT 55, i FROM
generate_series(-1000000, 1000000) i;" ), and then re-run the query,
the scan is exactly as fast as before -- it just skips to get over the
newly inserted "55" group of tuples. Obviously, this also makes the
master branch far, far slower.

As I've said many times already, the need to be flexible and offer
robust performance in cases where skipping is either very effective or
very ineffective *during the same index scan* seems very important to
me. This "55" variant of your test.sql test case is a great example of
the kinds of cases I was thinking about.

Yes, I agree. Therefore, even if I can't think of a way to prevent
regressions
or if I can only think of improvements that would significantly
sacrifice the
benefits of skip scan, I would still like to report any regression cases
if
they occur.

There may be a better way, such as the new idea you suggested, and I
think there
is room for discussion regarding how far we should go in handling
regressions,
regardless of whether we choose to accept regressions or sacrifice the
benefits of
skip scan to address them.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

Attachments:

test_for_v15.sqltext/plain; name=test_for_v15.sqlDownload
In reply to: Masahiro Ikeda (#48)
3 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Nov 20, 2024 at 4:04 AM Masahiro Ikeda <ikedamsh@oss.nttdata.com> wrote:

Thanks for your quick response!

Attached is v16. This is similar to v15, but the new
v16-0003-Fix-regressions* patch to fix the regressions is much less
buggy, and easier to understand.

Unlike v15, the experimental patch in v16 doesn't change anything
about which index pages are read by the scan -- not even in corner
cases. It is 100% limited to fixing the CPU overhead of maintaining
skip arrays uselessly *within* a leaf page. My extensive test suite
passes; it no longer shows any changes in "Buffers: N" for any of the
EXPLAIN (ANALYZE, BUFFERS) ... output that the tests look at. This is
what I'd expect.

I think that it will make sense to commit this patch as a separate
commit, immediately after skip scan itself is committed. It makes it
clear that, at least in theory, the new v16-0003-Fix-regressions*
patch doesn't change any behavior that's visible to code outside of
_bt_readpage/_bt_checkkeys/_bt_advance_array_keys.

I didn't come up with the idea. At first glance, your idea seems good
for all cases.

My approach of conditioning the new "beyondskip" behavior on
"has_skip_array && beyond_end_advance" is at least a good start.

The idea behind conditioning this behavior on having at least one
beyond_end_advance array advancement is pretty simple: in practice
that almost never happens during skip scans that actually end up
skipping (either via another _bt_first that redesends the index, or
via skipping "within the page" using the
_bt_checkkeys_look_ahead/pstate->skip mechanism). So that definitely
seems like a good general heuristic. It just isn't sufficient on its
own, as you have shown.

Actually, test.sql shows a performance improvement, and the performance
is almost the same as the master's seqscan. To be precise, the master's
performance is 10-20% better than the v15 patch because the seqscan is
executed in parallel. However, the v15 patch is twice as fast as when
seqscan is not executed in parallel.

I think that that's a good result, overall.

Bear in mind that a case such as this might receive a big performance
benefit if it can skip only once or twice. It's almost impossible to
model those kinds of effects within the optimizer's cost model, but
they're still important effects.

FWIW, I notice that your "t" test table is 35 MB, whereas its t_idx
index is 21 MB. That's not very realistic (the index size is usually a
smaller fraction of the table size than we see here), which probably
partly explains why the planner likes parallel sequential scan for
this.

However, I found that there is still a problematic case when I read your
patch. IIUC, beyondskip becomes true only if the tuple's id2 is greater
than the scan key value. Therefore, the following query (see
test_for_v15.sql)
still degrades.

As usual, you are correct. :-)

I’m reporting the above result, though you might already be aware of the
issue.

Thanks!

I have an experimental fix in mind for this case. One not-very-good
way to fix this new problem seems to work:

diff --git a/src/backend/access/nbtree/nbtutils.c
b/src/backend/access/nbtree/nbtutils.c
index b70b58e0c..ddae5f2a1 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -3640,7 +3640,7 @@ _bt_advance_array_keys(IndexScanDesc scan,
BTReadPageState *pstate,
      * for skip scan, and stop maintaining the scan's skip arrays until we
      * reach the page's finaltup, if any.
      */
-    if (has_skip_array && beyond_end_advance &&
+    if (has_skip_array && !all_required_satisfied &&
         !has_required_opposite_direction_skip && pstate->finaltup)
         pstate->beyondskip = true;

However, a small number of my test cases now fail. And (I assume) this
approach has certain downsides on leaf pages where we're now too quick
to stop maintaining skip arrays.

What I really need to do next is to provide a vigorous argument for
why the new pstate->beyondskip behavior is correct. I'm already
imposing restrictions on range skip arrays in v16 of the patch --
that's what the "!has_required_opposite_direction_skip" portion of the
test is about. But it still feels too ad-hoc.

I'm a little worried that these restrictions on range skip arrays will
themselves be the problem for some other kind of query. Imagine a
query like this:

SELECT * FROM t WHERE id1 BETWEEN 0 AND 1_000_000 AND id2 = 1

This is probably going to be regressed due to the aforementioned
"!has_required_opposite_direction_skip" restriction. Right now I don't
fully understand what restrictions are truly necessary, though. More
research is needed.

I think for v17 I'll properly fix all of the regressions that you've
complained about so far, including the most recent "SELECT * FROM t
WHERE id2 = 1_000_000" regression. Hopefully the best fix for this
other "WHERE id1 BETWEEN 0 AND 1_000_000 AND id2 = 1" regression will
become clearer once I get that far. What do you think?

Yes, I agree. Therefore, even if I can't think of a way to prevent
regressions
or if I can only think of improvements that would significantly
sacrifice the
benefits of skip scan, I would still like to report any regression cases
if
they occur.

You're right, of course. It might make sense to accept some very small
regressions. But not if we can basically avoid all regressions. Which
may well be an attainable goal.

There may be a better way, such as the new idea you suggested, and I
think there
is room for discussion regarding how far we should go in handling
regressions,
regardless of whether we choose to accept regressions or sacrifice the
benefits of
skip scan to address them.

There are definitely lots more options to address these regressions.
For example, we could have the planner hint that it thinks that skip
scan won't be a good idea, without that actually changing the basic
choices that nbtree makes about which pages it needs to scan (only how
to scan each individual leaf page). Or, we could remember that the
previous page used "pstate-> beyondskip" each time _bt_readpage reads
another page. I could probably think of 2 or 3 more ideas like that,
if I had to.

However, the problem is not a lack of ideas IMV. The important
trade-off is likely to be the trade-off between how effectively we can
avoid these regressions versus how much complexity each approach
imposes. My guess is that complexity is more likely to impose limits
on us than overall feasibility.

--
Peter Geoghegan

Attachments:

v16-0003-Fix-regressions-in-unsympathetic-skip-scan-cases.patchapplication/octet-stream; name=v16-0003-Fix-regressions-in-unsympathetic-skip-scan-cases.patchDownload
From c2e08b1dc180e3684afaa3f0bfd6fead0b401d7b Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v16 3/3] Fix regressions in unsympathetic skip scan cases.

Fix regressions in cases that are nominally eligible to use skip scan
but can never actually benefit from skipping.  These are cases where the
leading skipped prefix column contains many distinct values -- often as
many distinct values are there are total index tuples.

For example, the test case posted here is fixed by the work from this
commit:

https://postgr.es/m/51d00219180323d121572e1f83ccde2a@oss.nttdata.com

Note that this commit doesn't actually change anything about when or how
skip scan decides when or how to skip.  It just avoids wasting CPU
cycles on uselessly maintaining a skip array at the tuple granularity,
preferring to maintain the skip arrays at something closer to the page
granularity when that makes sense.  See:

https://www.postgresql.org/message-id/flat/CAH2-Wz%3DE7XrkvscBN0U6V81NK3Q-dQOmivvbEsjG-zwEfDdFpg%40mail.gmail.com#7d34e8aa875d7a718043834c5ef4c167

Doing well on cases like this is important because we can't expect the
optimizer to never choose an affected plan -- we prefer to solve these
problems in the executor, which has access to the most reliable and
current information about the index.  The optimizer can afford to be
very optimistic about skipping if actual runtime scan behavior is very
similar to a traditional full index scan in the worst case.  See
"optimizer" section from the original intro mail for more information:

https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP%2BG4bw%40mail.gmail.com

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h           |  5 +-
 src/backend/access/nbtree/nbtsearch.c |  5 ++
 src/backend/access/nbtree/nbtutils.c  | 89 +++++++++++++++++++++------
 3 files changed, 78 insertions(+), 21 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index d841e85bc..06ccab8cf 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1112,11 +1112,12 @@ typedef struct BTReadPageState
 	bool		firstmatch;		/* at least one match so far?  */
 
 	/*
-	 * Private _bt_checkkeys state used to manage "look ahead" optimization
-	 * (only used during scans with array keys)
+	 * Private _bt_checkkeys state used to manage "look ahead" and skip array
+	 * optimizations (only used during scans with array keys)
 	 */
 	int16		rechecks;
 	int16		targetdistance;
+	bool		beyondskip;
 
 } BTReadPageState;
 
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 43e321896..578bafbf4 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1646,6 +1646,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.firstmatch = false;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
+	pstate.beyondskip = false;
 
 	/*
 	 * Prechecking the value of the continuescan flag for the last item on the
@@ -1837,6 +1838,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
+			pstate.beyondskip = false; /* reset for finaltup */
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -1893,6 +1895,9 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			else
 				tuple_alive = true;
 
+			if (offnum == minoff)
+				pstate.beyondskip = false; /* reset for finaltup */
+
 			itup = (IndexTuple) PageGetItem(page, iid);
 			Assert(!BTreeTupleIsPivot(itup));
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 9de7c40e8..b70b58e0c 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -151,7 +151,8 @@ static bool _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption);
 static void _bt_mark_scankey_required(ScanKey skey);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool beyondskip,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
@@ -2343,7 +2344,6 @@ _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
 	Form_pg_attribute attr;
 
 	Assert(skey->sk_flags & SK_SEARCHARRAY);
-	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
 
 	/* Regular (non-skip) array? */
 	if (array->num_elems != -1)
@@ -2489,7 +2489,6 @@ _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
 	Form_pg_attribute attr;
 
 	Assert(skey->sk_flags & SK_SEARCHARRAY);
-	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
 
 	/* Regular (non-skip) array? */
 	if (array->num_elems != -1)
@@ -3135,7 +3134,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	ScanDirection dir = so->currPos.dir;
 	int			arrayidx = 0;
 	bool		beyond_end_advance = false,
+				has_skip_array = false,
 				has_required_opposite_direction_only = false,
+				has_required_opposite_direction_skip = false,
 				oppodir_inequality_sktrig = false,
 				all_required_satisfied = true,
 				all_satisfied = true;
@@ -3189,6 +3190,20 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			{
 				array = &so->arrayKeys[arrayidx++];
 				Assert(array->scan_key == ikey);
+				if (array->num_elems == -1)
+				{
+					has_skip_array = true;
+					if (!array->null_elem)
+					{
+						if (ScanDirectionIsForward(dir) ?
+							array->low_compare : array->high_compare)
+							has_required_opposite_direction_skip = true;
+						if (ScanDirectionIsForward(dir) ?
+							(cur->sk_flags & SK_BT_NULLS_FIRST) :
+							!(cur->sk_flags & SK_BT_NULLS_FIRST))
+							has_required_opposite_direction_skip = true;
+					}
+				}
 			}
 		}
 		else
@@ -3211,8 +3226,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -3496,7 +3509,8 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * higher-order arrays (might exhaust all the scan's arrays instead, which
 	 * ends the top-level scan).
 	 */
-	if (beyond_end_advance && !_bt_advance_array_keys_increment(scan, dir))
+	if (beyond_end_advance && sktrig_required &&
+		!_bt_advance_array_keys_increment(scan, dir))
 		goto end_toplevel_scan;
 
 	Assert(_bt_verify_keys_with_arraykeys(scan));
@@ -3528,7 +3542,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -3615,9 +3629,20 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * for _bt_check_compare to behave as if they are required in the current
 	 * scan direction to deal with NULLs.  We'll account for that separately.)
 	 */
-	Assert(_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts,
-										false, 0, NULL) ==
-		   !all_required_satisfied);
+	Assert((_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts,
+										 false, 0, NULL) ==
+			!all_required_satisfied) || pstate->beyondskip);
+
+	/*
+	 * Optimization: if a scan with a skip array required "beyond end of array
+	 * element" array advancement (not necessarily in respect of the skip
+	 * array itself), we assume that the page isn't a particularly good target
+	 * for skip scan, and stop maintaining the scan's skip arrays until we
+	 * reach the page's finaltup, if any.
+	 */
+	if (has_skip_array && beyond_end_advance &&
+		!has_required_opposite_direction_skip && pstate->finaltup)
+		pstate->beyondskip = true;
 
 	/*
 	 * We generally permit primitive index scans to continue onto the next
@@ -3867,7 +3892,8 @@ end_toplevel_scan:
  * make the scan much more efficient: if there are few distinct values in "x",
  * we'll be able to skip over many irrelevant leaf pages.  (If on the other
  * hand there are many distinct values in "x" then the scan will degenerate
- * into a full index scan at run time.)
+ * into a full index scan at run time, but we'll be no worse off overall.
+ * _bt_checkkeys's 'beyondskip' optimization keeps the runtime overhead low.)
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -4908,7 +4934,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+							arrayKeys, pstate->beyondskip,
+							pstate->prechecked, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
@@ -4920,7 +4947,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
 		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
+		Assert(!pstate->beyondskip && !pstate->prechecked &&
+			   !pstate->firstmatch);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
@@ -4934,7 +4962,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, pstate->beyondskip,
+										false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -5065,7 +5094,7 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	Assert(so->numArrayKeys);
 
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -5110,11 +5139,19 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Callers with skip arrays can pass beyondskip=true to have us assume that
+ * the scan is still likely to be before the current array keys according to
+ * _bt_tuple_before_array_skeys.  We can safely avoid evaluating skip array
+ * scan keys when this happens.  Note that this makes us treat any required
+ * SAOP arrays as non-required -- skip scan caller is expected to disable this
+ * behavior upon reaching the page's finaltup.
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool beyondskip,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -5131,10 +5168,24 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction -- though
+		 * not when "beyond end advancement" skip scan optimization is in use
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (beyondskip)
+		{
+			/*
+			 * "Beyond end advancement" skip scan optimization.
+			 *
+			 * Just skip over any skip array scan keys.  Treat all other scan
+			 * keys as not required for the scan to continue.
+			 */
+			Assert(!prechecked);
+
+			if (key->sk_flags & SK_BT_SKIP)
+				continue;
+		}
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
-- 
2.45.2

v16-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v16-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From 5610b123f46b0f19514d4945ad7b0bafbaf8f99c Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v16 1/3] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 contrib/bloom/blscan.c                        |   1 +
 doc/src/sgml/bloom.sgml                       |   6 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 22 files changed, 243 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index e1884acf4..7b4180db5 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -153,6 +153,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index c0b978119..52939d317 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -585,6 +585,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index f2fd62afb..5e423e155 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index b35b8a975..36f1435cb 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index 0d99d6abc..927ba1039 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 60c61039d..ec259012b 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index dd76fe1da..8bbb3d734 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -69,6 +69,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -552,6 +553,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -578,6 +580,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -705,6 +708,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			Assert(btscan->btps_nextScanPage != P_NONE);
 			*next_scan_page = btscan->btps_nextScanPage;
@@ -805,6 +813,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -839,6 +849,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 0cd046613..82bb93d1a 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -970,6 +970,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 301786185..be668abf2 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7c0fd63b2..6cb5ebcd2 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2098,6 +2100,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2111,6 +2114,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2127,6 +2131,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2645,6 +2650,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index 0c5fb725e..f77f716f0 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -116,6 +116,7 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++)
 	{
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 19f2b172c..92b13f539 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -170,9 +170,10 @@ CREATE INDEX
    Heap Blocks: exact=28
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..1792.00 rows=2 width=0) (actual time=0.356..0.356 rows=29 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 0.408 ms
-(8 rows)
+(9 rows)
 </programlisting>
   </para>
 
@@ -202,11 +203,12 @@ CREATE INDEX
    -&gt;  BitmapAnd  (cost=24.34..24.34 rows=2 width=0) (actual time=0.027..0.027 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..12.04 rows=500 width=0) (actual time=0.026..0.026 rows=0 loops=1)
                Index Cond: (i5 = 123451)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
-(9 rows)
+(10 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 840d7f816..c68eb770e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4198,12 +4198,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index cd12b9ce4..482715397 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -727,8 +727,10 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Heap Blocks: exact=10
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 1
  Planning Time: 0.485 ms
  Execution Time: 0.073 ms
 </screen>
@@ -779,6 +781,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Heap Blocks: exact=90
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
  Planning Time: 0.187 ms
  Execution Time: 3.036 ms
 </screen>
@@ -844,6 +847,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -873,9 +877,11 @@ EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique
          Buffers: shared hit=4 read=3
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared hit=2
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
                Buffers: shared hit=2 read=3
  Planning:
    Buffers: shared hit=3
@@ -908,6 +914,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Heap Blocks: exact=90
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1042,6 +1049,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
  Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
  Planning Time: 0.077 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a854..e042638b7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -502,9 +502,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    Batches: 1  Memory Usage: 24kB
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(7 rows)
+(8 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index ae9ce9d8e..c24d56007 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index f6b8329cd..a8bc0bfd7 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7a03b4e36..e3e99272a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2657,47 +2661,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off)
@@ -2713,16 +2726,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2741,7 +2757,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off)
@@ -2757,16 +2773,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2787,7 +2806,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2877,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2897,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,17 +2986,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2982,17 +3013,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3064,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3048,17 +3091,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3112,17 +3161,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3144,17 +3199,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3482,12 +3543,14 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(15);
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3566,10 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(25);
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3617,17 @@ explain (analyze, costs off, summary off, timing off) select * from ma_test wher
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4197,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 33a6dceb0..b7cf35b9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index 2eaeb1477..9afe205e0 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 442428d93..085e746af 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

v16-0002-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v16-0002-Add-skip-scan-to-nbtree.patchDownload
From 3b55e02c4a962646cd177e3f870981e8c1187242 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v16 2/3] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
useful in cases where the total number of distinct values in the column
'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.

The design of skip arrays allows array advancement to work without
concern for which specific array types are involved; only the lowest
level code needs to treat skip arrays and SAOP arrays differently.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values are conceptually just like any
other value that might appear in either kind of array.  A call made to
_bt_first to reposition the scan based on a NEXT/PRIOR sentinel usually
won't locate a leaf page containing tuples that must be returned to the
scan, but that's normal during any skip scan that happens to involve
"sparse" indexed values (skip support can only help with "dense" indexed
values, which isn't meaningfully possible when the skipped column is of
a continuous type, where skip support typically can't be implemented).

The optimizer doesn't use distinct new index paths to represent index
skip scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  Skipping is possible during
eligible bitmap index scans, index scans, and index-only scans.  It's
also possible during eligible parallel scans.  An eligible scan is any
scan that nbtree preprocessing generates a skip array for, which happens
whenever doing so will enable later preprocessing to mark original input
scan keys (passed by the executor) as required to continue the scan.

There are hardly any limitations around where skip arrays/scan keys may
appear relative to conventional/input scan keys.  This is no less true
in the presence of conventional SAOP array scan keys, which may both
roll over and be rolled over by skip arrays.  For example, a skip array
on the column "b" is generated for "WHERE a = 42 AND c IN (5, 6, 7, 8)".
As always (since commit 5bf748b8), whether or not nbtree actually skips
depends in large part on physical index characteristics at runtime.
Preprocessing is optimistic about skipping working out: it applies
simple static rules to decide where to place skip arrays.  It's up to
array-related scan code to keep the overhead of maintaining the scan's
skip arrays under control when it turns out that skipping isn't helpful.

Preprocessing will never cons up a skip array for an index column that
has an equality strategy scan key on input, but will do so for indexed
columns that are only constrained by inequality strategy scan keys.
This allows the scan to skip using a range skip array.  These are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   28 +-
 src/include/catalog/pg_amproc.dat             |   22 +
 src/include/catalog/pg_proc.dat               |   27 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  101 +
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  273 +++
 src/backend/access/nbtree/nbtree.c            |  213 ++-
 src/backend/access/nbtree/nbtsearch.c         |   75 +-
 src/backend/access/nbtree/nbtutils.c          | 1695 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   46 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  379 +++-
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/timestamp.c             |   48 +
 src/backend/utils/adt/uuid.c                  |   71 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/bloom.sgml                       |    1 +
 doc/src/sgml/btree.sgml                       |   34 +-
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   49 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |   10 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   61 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    5 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 36 files changed, 2976 insertions(+), 333 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c51de742e..0826c124f 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -202,7 +202,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 123fba624..d841e85bc 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1022,10 +1024,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* skip scan support (unless !use_sksup) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo   *low_order;		/* low_compare's ORDER proc */
+	FmgrInfo   *high_order;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1113,6 +1127,10 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on omitted prefix column */
+#define SK_BT_NEGPOSINF	0x00080000	/* -inf/+inf key (invalid sk_argument) */
+#define SK_BT_NEXT		0x00100000	/* positions scan > sk_argument */
+#define SK_BT_PRIOR		0x00200000	/* positions scan < sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1149,6 +1167,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1159,7 +1181,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5d7fe292b..c5af25806 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cbbe8acd3..abfe92666 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2260,6 +2275,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4447,6 +4465,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6290,6 +6311,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9327,6 +9351,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d70e6d37e..5b58739ad 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..d97e6059d
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,101 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining pairs of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code falls back on next-key sentinel values for any opclass that
+ * doesn't provide its own skip support function.  There is no benefit to
+ * providing skip support for an opclass on a type where guessing that the
+ * next indexed value is the next possible indexable value seldom works out.
+ * It might not even be feasible to add skip support for a continuous type.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order).
+	 * This gives the B-Tree code a useful value to start from, before any
+	 * data has been read from the index.  These are also sometimes used to
+	 * detect when the scan has run out of indexable values to search for.
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 *
+	 * Operator class decrement/increment functions never have to directly
+	 * deal with the value NULL, nor with ASC vs. DESC column ordering.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 1859be614..b4f1466d4 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..26ebbf776 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 8bbb3d734..105bcfe2f 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -29,6 +29,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -70,7 +71,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -78,11 +79,23 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -535,10 +548,159 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume all input scankeys will be output with its own
+	 * SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * scan array (and associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		Form_pg_attribute attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array/skip scan key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array/skip scan key, while freeing memory allocated
+		 * for old sk_argument where required
+		 */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -549,7 +711,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -572,16 +735,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -608,6 +771,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -652,7 +816,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -674,14 +838,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -719,7 +878,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -763,11 +922,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -806,7 +965,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -815,7 +974,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -833,6 +992,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -842,7 +1002,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -852,14 +1012,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 82bb93d1a..43e321896 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -984,6 +984,13 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  Skip
+	 * array keys are = keys, though we'll sometimes need to treat the key as
+	 * if it was some kind of inequality instead.  This is required whenever
+	 * the skip array's current array element is a sentinel value.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1059,8 +1066,39 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is -inf or +inf, choose low_compare/high_compare
+				 * (or consider if they imply a NOT NULL constraint).
+				 */
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skiparraysk = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					if (!array->null_elem)
+						impliesNN = skiparraysk;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1097,6 +1135,39 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					strat_total == BTLessStrategyNumber)
 					break;
 
+				/*
+				 * If this is a scan key for a skip array whose current
+				 * element is marked NEXT or PRIOR, adjust strat_total
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(strat_total == BTEqualStrategyNumber);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[].
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).
+					 */
+					break;
+				}
+
 				/*
 				 * Done if that was the last attribute, or if next key is not
 				 * in sequence (implying no boundary key is available for the
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 896696ff7..9de7c40e8 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	Oid			eq_op;			/* = op to be used to add array, if any */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -63,16 +91,46 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
+static int	_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+										  int *numSkipArrayKeys);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_decrement(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_increment(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -256,6 +314,12 @@ _bt_freestack(BTStack stack)
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -271,10 +335,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -282,30 +348,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_preprocess_num_array_keys(scan, skipatts,
+												 &numSkipArrayKeys);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys described within skipatts[]
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -320,17 +380,18 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/* Process input keys, and emit skip array keys described by skipatts[] */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -346,19 +407,97 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and its scan key where indicated */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* attribute already had an "=" key on input */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[numArrayKeyData];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].low_order = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].high_order = NULL;	/* for now */
+
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how this
+			 * constructed "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			numArrayKeyData++;	/* keep this scan key/array */
+		}
+
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
+		cur = &arrayKeyData[numArrayKeyData];
 		*cur = scan->keyData[input_ikey];
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -414,7 +553,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -425,7 +564,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -442,7 +581,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -517,23 +656,234 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].low_order = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].high_order = NULL;	/* unused */
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
 	return arrayKeyData;
 }
 
+/*
+ *	_bt_preprocess_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit all required so->arrayKeys[] entries.
+ *
+ * Also sets *numSkipArrayKeys to the number of skip arrays that caller must
+ * make sure to add to the scan keys it'll output (complete with corresponding
+ * so->arrayKeys[] entries), and sets the corresponding attributes positions
+ * in *skipatts argument.  Every attribute that receives a skip array will
+ * have its skip support routine set in *skipatts, too.
+ */
+static int
+_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+							  int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_inkey = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				(*numSkipArrayKeys)++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				break;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatt
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatt->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										 BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack even an equality strategy
+	 * operator, but they're supported.  We cope by making caller not use a
+	 * skip array for this index column (nor for any lower-order columns).
+	 */
+	if (!OidIsValid(skipatt->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatt->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+													   reverse,
+													   &skipatt->sksup);
+
+	/* might not have set up skip support, but can skip either way */
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys_final() -- fix up array scan key references
  *
@@ -633,7 +983,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -669,6 +1020,14 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && !array->null_elem)
+						_bt_skip_preproc_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
@@ -684,7 +1043,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -980,26 +1339,28 @@ _bt_merge_arrays(IndexScanDesc scan, ScanKey skey, FmgrInfo *sortproc,
  * guaranteeing that at least the scalar scan key can be considered redundant.
  * We return false if the comparison could not be made (caller must keep both
  * scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar inequality scan keys.
  */
 static bool
 _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
 	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
 		   skey->sk_strategy != BTEqualStrategyNumber);
@@ -1009,8 +1370,7 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1041,11 +1401,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensures that the scalar scan key can be eliminated as redundant
+		 */
+		eliminated = true;
+
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+	}
+	else
+	{
+		/*
+		 * With a skip array, it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when there's a lack of suitable cross-type support for
+		 * comparing skey to an existing inequality associated with the array.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when
+ * determining its redundancy against a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1097,10 +1511,298 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of a skip array scan key when determining its
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array.  This forces the array's elements to be generated within the limits
+ * of a range that's described/constrained by the array's inequalities.
+ *
+ * Return value indicates if the scalar scan key could be "eliminated".  We
+ * return true in the common case where caller's scan key was successfully
+ * rolled into the skip array.  We return false when we can't do that due to
+ * the presence of an existing, conflicting inequality that cannot be compared
+ * to caller's inequality due to a lack of suitable cross-type support.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way scan code can generally assume that
+	 * it'll always be safe to use the ORDER procs stored in so->orderProcs[].
+	 * The only exception is cases where the array's scan key is NEGPOSINF.
+	 *
+	 * When the array's scan key is marked NEGPOSINF, then it'll lack a valid
+	 * sk_argument.  The scan must apply the array's low_compare and/or
+	 * high_compare (if any) to establish whether a given tupdatum is within
+	 * the range of the skip array.  This could involve applying the 3-way
+	 * low_order/high_order ORDER proc we store here (though never the ORDER
+	 * proc stored in so->orderProcs[]).
+	 *
+	 * Note: we sometimes use low_compare/high_compare inequalities with the
+	 * array's current element value, and with values that were mutated by the
+	 * array's skip support routine.  A skip array must always use its index
+	 * attribute's own input opclass/type, so this will always work correctly.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (!array->high_compare)
+			{
+				/* array currently lacks a high_compare, make space for one */
+				array->high_compare = palloc(sizeof(ScanKeyData));
+				array->high_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare, keep old one */
+
+				/* replace old high_compare with caller's new one */
+			}
+
+			/* caller's high_compare becomes skip array's high_compare */
+			*array->high_compare = *skey;
+			*array->high_order = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (!array->low_compare)
+			{
+				/* array currently lacks a low_compare, make space for one */
+				array->low_compare = palloc(sizeof(ScanKeyData));
+				array->low_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare, keep old one */
+
+				/* replace old low_compare with caller's new one */
+			}
+
+			/* caller's low_compare becomes skip array's low_compare */
+			*array->low_compare = *skey;
+			*array->low_order = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
 
+/*
+ * Perform final preprocessing for a skip array.  Called when the skip array's
+ * final low_compare and high_compare have been chosen.
+ *
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts the
+ * operator strategies).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value NEGPOSINF, using >= instead of using >
+ * (or using <= instead of < during backwards scans) makes it safe to include
+ * lower-order scan keys in the insertion scan key (there must be lower-order
+ * scan keys after the skip array).  We will avoid an extra _bt_first to find
+ * the first value in the index > sk_argument, at least when the first real
+ * matching value in the index happens to be an exact match for the newly
+ * transformed (newly incremented/decremented) sk_argument value.
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01' -- which would be wrong.
+ */
+static void
+_bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+	Assert(!array->null_elem);
+
+	/* If skip array doesn't have skip support, can't apply optimization */
+	if (!array->use_sksup)
+		return;
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skip_preproc_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skip_preproc_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skip_preproc_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+	Assert(array->use_sksup);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type.
+	 *
+	 * Note: we have to support the convention that sk_subtype == InvalidOid
+	 * means the opclass input type.
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup.decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skip_preproc_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+	Assert(array->use_sksup);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type.
+	 *
+	 * Note: we have to support the convention that sk_subtype == InvalidOid
+	 * means the opclass input type.
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup.increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  * qsort_arg comparator for sorting array elements
  */
@@ -1220,6 +1922,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1342,6 +2046,98 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, because the array doesn't really
+ * have any elements (it generates its array elements procedurally instead).
+ * Note that this may include a NULL value/an IS NULL qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ *
+ * Note: This function doesn't actually binary search.  It uses an interface
+ * that's as similar to _bt_binsrch_array_skey as possible for consistency.
+ * "Binary searching a skip array" just means determining if tupdatum/tupnull
+ * are before, within, or beyond the range of caller's skip array.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1351,29 +2147,480 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting skip array to lowest element, which isn't just NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Setting skip array to highest element, which isn't just NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element is out of bounds for the array.
+		 *
+		 * This is only possible if low_compare represents an >= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the prior value cannot possibly satisfy low_compare, so we can
+		 * give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(array->low_order,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Decrement by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &uflow);
+	if (uflow)
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the bounds of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element is out of bounds for the array.
+		 *
+		 * This is only possible if high_compare represents an <= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the next value cannot possibly satisfy high_compare, so we can
+		 * give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(array->high_order,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Increment by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &oflow);
+	if (oflow)
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the bounds of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1389,6 +2636,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1398,29 +2646,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1480,6 +2729,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1487,7 +2737,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1499,16 +2748,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1633,9 +2876,93 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_NEGPOSINF))
+		{
+			/* Scankey has a valid/comparable sk_argument value (not -inf) */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXT))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument + infinitesimal"
+				 */
+				if (ScanDirectionIsForward(dir))
+				{
+					/* tupdatum is still < "sk_argument + infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was forward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to backward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+			else if (result == 0 && (cur->sk_flags & SK_BT_PRIOR))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument - infinitesimal"
+				 */
+				if (ScanDirectionIsBackward(dir))
+				{
+					/* tupdatum is still > "sk_argument - infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was backward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to forward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+		}
+		else
+		{
+			/*
+			 * Scankey searches for the sentinel value -inf (or +inf).
+			 *
+			 * Note: -inf could mean "true negative infinity", or it could
+			 * just represent the lowest/first value that satisfies the skip
+			 * array's low_compare.  +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * "Compare tupdatum against -inf" using array's low_compare, if
+			 * any (or "compare it against +inf" using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is >= -inf and <= +inf.  It's time for caller to
+				 * advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1967,18 +3294,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -2003,18 +3321,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2032,12 +3341,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -2113,11 +3431,62 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  "Binary searching" only determined whether
+		 * tupdatum is beyond, before, or within the range of the skip array.
+		 *
+		 * As always, we set the array element to its closest available match.
+		 * But unlike with a conventional array, a skip array's new element
+		 * might be NEGPOSINF, which represents the lowest (or the highest)
+		 * real value that is within the range of the skip array.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == "some array element".
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2467,6 +3836,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2479,10 +3850,24 @@ end_toplevel_scan:
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never consed up skip array scan keys, it would be possible for "gaps"
+ * to appear that made it unsafe to mark any subsequent input scan keys (taken
+ * from scan->keyData[]) as required to continue the scan.  If there are no
+ * keys for a given attribute, the keys for subsequent attributes can never be
+ * required.  For instance, "WHERE y = 4" always required a full-index scan
+ * prior to Postgres 18.  Typically, preprocessing is now able to "rewrite"
+ * such a qual into "WHERE x = ANY('{every possible x value}') and y = 4".
+ * The addition of a consed up skip array key on "x" enables marking the key
+ * on "y" (as well as the one on "x") required, according to the usual rules.
+ * Of course, this transformation process cannot change the tuples returned by
+ * the scan -- the skip array will use an IS NULL qual when searching for its
+ * "NULL element" (in this particular example, at least).  But skip arrays can
+ * make the scan much more efficient: if there are few distinct values in "x",
+ * we'll be able to skip over many irrelevant leaf pages.  (If on the other
+ * hand there are many distinct values in "x" then the scan will degenerate
+ * into a full index scan at run time.)
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -2519,7 +3904,12 @@ end_toplevel_scan:
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -2583,6 +3973,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -2671,7 +4069,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -2728,7 +4127,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -2776,6 +4174,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * Typically, our call to _bt_preprocess_array_keys will have
+			 * already added "=" skip array keys as required to form an
+			 * unbroken series of "=" constraints on all attrs prior to the
+			 * attr from the last scan key that came from our scan->keyData[]
+			 * input scan keys.  However, certain implementation restrictions
+			 * might have prevented array preprocessing from doing that for us
+			 * (e.g., opclasses without an "=" operator can't use skip scan).
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -2864,6 +4270,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -2872,6 +4279,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -2891,8 +4299,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -3034,10 +4440,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3103,6 +4510,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -3112,6 +4522,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3185,6 +4611,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3746,6 +5173,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index db6ed784a..86a5de8f4 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 16144c2b7..2cef9816e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -370,6 +370,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 85e5eaf32..88c929a0a 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -98,6 +98,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 8130f3e8a..256350007 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f73f294b8..cb8470324 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -85,6 +85,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 08fa6774d..42fde7ef2 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -192,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -213,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5736,6 +5740,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6794,6 +6884,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6803,17 +6940,22 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6829,14 +6971,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -6846,13 +6993,90 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't absord a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6874,6 +7098,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6894,7 +7119,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6910,6 +7135,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6925,6 +7182,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -7033,104 +7291,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index e00cd3c96..213a6326f 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2286,6 +2287,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 5284d23dc..654bda638 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -384,6 +387,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 8a67f0120..6befee3a4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1751,6 +1752,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3590,6 +3602,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 92b13f539..6fa688769 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -206,6 +206,7 @@ CREATE INDEX
                Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
+               Index Searches: 0
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
 (10 rows)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index d3358dfc3..fa6f27e6d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1938,7 +1938,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -1958,7 +1958,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 9b2973694..4c4909691 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4440,24 +4440,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -7562,19 +7563,23 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                        QUERY PLAN                         
------------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Nested Loop
    ->  Hash Join
          Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-         ->  Seq Scan on fkest f2
-               Filter: (x100 = 2)
+         ->  Bitmap Heap Scan on fkest f2
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
          ->  Hash
-               ->  Seq Scan on fkest f1
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f1
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
          Index Cond: (x = f1.x)
-(10 rows)
+(14 rows)
 
 alter table fkest add constraint fk
   foreign key (x, x10b, x100) references fkest (x, x10, x100);
@@ -7583,20 +7588,24 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                     QUERY PLAN                      
------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Hash Join
    Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
    ->  Hash Join
          Hash Cond: (f3.x = f2.x)
          ->  Seq Scan on fkest f3
          ->  Hash
-               ->  Seq Scan on fkest f2
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f2
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Hash
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-(11 rows)
+         ->  Bitmap Heap Scan on fkest f1
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
+(15 rows)
 
 rollback;
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 36dc31c16..aef239200 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5240,9 +5240,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index c73631a9a..62eb4239a 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1461,18 +1461,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index fe162cc7c..dea40e013 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -766,7 +766,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -776,7 +776,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 08521d51a..89f021ccd 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2666,6 +2667,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

#50Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Peter Geoghegan (#49)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 2024-11-21 04:40, Peter Geoghegan wrote:

On Wed, Nov 20, 2024 at 4:04 AM Masahiro Ikeda
<ikedamsh@oss.nttdata.com> wrote:

Thanks for your quick response!

Attached is v16. This is similar to v15, but the new
v16-0003-Fix-regressions* patch to fix the regressions is much less
buggy, and easier to understand.

Unlike v15, the experimental patch in v16 doesn't change anything
about which index pages are read by the scan -- not even in corner
cases. It is 100% limited to fixing the CPU overhead of maintaining
skip arrays uselessly *within* a leaf page. My extensive test suite
passes; it no longer shows any changes in "Buffers: N" for any of the
EXPLAIN (ANALYZE, BUFFERS) ... output that the tests look at. This is
what I'd expect.

I think that it will make sense to commit this patch as a separate
commit, immediately after skip scan itself is committed. It makes it
clear that, at least in theory, the new v16-0003-Fix-regressions*
patch doesn't change any behavior that's visible to code outside of
_bt_readpage/_bt_checkkeys/_bt_advance_array_keys.

Thanks for the update! I'll look into the details and understand the
approach to the commit.

I didn't come up with the idea. At first glance, your idea seems good
for all cases.

My approach of conditioning the new "beyondskip" behavior on
"has_skip_array && beyond_end_advance" is at least a good start.

The idea behind conditioning this behavior on having at least one
beyond_end_advance array advancement is pretty simple: in practice
that almost never happens during skip scans that actually end up
skipping (either via another _bt_first that redesends the index, or
via skipping "within the page" using the
_bt_checkkeys_look_ahead/pstate->skip mechanism). So that definitely
seems like a good general heuristic. It just isn't sufficient on its
own, as you have shown.

Yes, I think so. This idea can make the worst-case execution time of a
skip scan almost the same as that of a full index scan.

Actually, test.sql shows a performance improvement, and the
performance
is almost the same as the master's seqscan. To be precise, the
master's
performance is 10-20% better than the v15 patch because the seqscan is
executed in parallel. However, the v15 patch is twice as fast as when
seqscan is not executed in parallel.

I think that that's a good result, overall.

Bear in mind that a case such as this might receive a big performance
benefit if it can skip only once or twice. It's almost impossible to
model those kinds of effects within the optimizer's cost model, but
they're still important effects.

FWIW, I notice that your "t" test table is 35 MB, whereas its t_idx
index is 21 MB. That's not very realistic (the index size is usually a
smaller fraction of the table size than we see here), which probably
partly explains why the planner likes parallel sequential scan for
this.

Yes, I agree that the above case is not realistic. If anything, the
purpose of this case might be to simply find regression scenarios.

One possible use case I can think of is enforcing a unique constraint
on all columns. However, such cases are probably very rare.

I have an experimental fix in mind for this case. One not-very-good
way to fix this new problem seems to work:

diff --git a/src/backend/access/nbtree/nbtutils.c
b/src/backend/access/nbtree/nbtutils.c
index b70b58e0c..ddae5f2a1 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -3640,7 +3640,7 @@ _bt_advance_array_keys(IndexScanDesc scan,
BTReadPageState *pstate,
* for skip scan, and stop maintaining the scan's skip arrays 
until we
* reach the page's finaltup, if any.
*/
-    if (has_skip_array && beyond_end_advance &&
+    if (has_skip_array && !all_required_satisfied &&
!has_required_opposite_direction_skip && pstate->finaltup)
pstate->beyondskip = true;

However, a small number of my test cases now fail. And (I assume) this
approach has certain downsides on leaf pages where we're now too quick
to stop maintaining skip arrays.

Since I've built with the above change and executed make check, I found
that there is an assertion error, which may not be related to what you
pointed
out.

* the reproducible simple query (you can see the original query in
opr_sanity.sql).
select * from pg_proc
where proname in (
'lo_lseek64',
'lo_truncate',
'lo_truncate64')
and pronamespace = 11;

* the assertion error
TRAP: failed Assert("sktrig_required && required"), File:
"nbtutils.c", Line: 3375, PID: 362411

While investigating the error, I thought we might need to consider
whether key->sk_flags does not have SK_BT_SKIP. The assertion error
occurs because
requiredSameDir doesn't become true since proname does not have
SK_BT_SKIP.

+		if (beyondskip)
+		{
+			/*
+			 * "Beyond end advancement" skip scan optimization.
+			 *
+			 * Just skip over any skip array scan keys.  Treat all other scan
+			 * keys as not required for the scan to continue.
+			 */
+			Assert(!prechecked);
+
+			if (key->sk_flags & SK_BT_SKIP)
+				continue;
+		}
+		else if (((key->sk_flags & SK_BT_REQFWD) && 
ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
  			requiredSameDir = true;

What I really need to do next is to provide a vigorous argument for
why the new pstate->beyondskip behavior is correct. I'm already
imposing restrictions on range skip arrays in v16 of the patch --
that's what the "!has_required_opposite_direction_skip" portion of the
test is about. But it still feels too ad-hoc.

I'm a little worried that these restrictions on range skip arrays will
themselves be the problem for some other kind of query. Imagine a
query like this:

SELECT * FROM t WHERE id1 BETWEEN 0 AND 1_000_000 AND id2 = 1

This is probably going to be regressed due to the aforementioned
"!has_required_opposite_direction_skip" restriction. Right now I don't
fully understand what restrictions are truly necessary, though. More
research is needed.

I think for v17 I'll properly fix all of the regressions that you've
complained about so far, including the most recent "SELECT * FROM t
WHERE id2 = 1_000_000" regression. Hopefully the best fix for this
other "WHERE id1 BETWEEN 0 AND 1_000_000 AND id2 = 1" regression will
become clearer once I get that far. What do you think?

To be honest, I don't fully understand
has_required_opposite_direction_skip
and its context at the moment. Please give me some time to understand
it,
and I'd like to provide feedback afterward.

FWIW, the optimization is especially important for types that don't
support
'skipsupport', like 'real'. Although the example case I provided uses
integer
types, they (like 'real') tend to have many different values and high
cardinality, which means the possibility of skip scan working
efficiently
can be low.

There may be a better way, such as the new idea you suggested, and I
think there
is room for discussion regarding how far we should go in handling
regressions,
regardless of whether we choose to accept regressions or sacrifice the
benefits of
skip scan to address them.

There are definitely lots more options to address these regressions.
For example, we could have the planner hint that it thinks that skip
scan won't be a good idea, without that actually changing the basic
choices that nbtree makes about which pages it needs to scan (only how
to scan each individual leaf page). Or, we could remember that the
previous page used "pstate-> beyondskip" each time _bt_readpage reads
another page. I could probably think of 2 or 3 more ideas like that,
if I had to.

However, the problem is not a lack of ideas IMV. The important
trade-off is likely to be the trade-off between how effectively we can
avoid these regressions versus how much complexity each approach
imposes. My guess is that complexity is more likely to impose limits
on us than overall feasibility.

OK, I think you're right.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#51Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Masahiro Ikeda (#50)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 2024-11-21 17:47, Masahiro Ikeda wrote:

On 2024-11-21 04:40, Peter Geoghegan wrote:
diff --git a/src/backend/access/nbtree/nbtutils.c
b/src/backend/access/nbtree/nbtutils.c
index b70b58e0c..ddae5f2a1 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -3640,7 +3640,7 @@ _bt_advance_array_keys(IndexScanDesc scan,
BTReadPageState *pstate,
* for skip scan, and stop maintaining the scan's skip arrays 
until we
* reach the page's finaltup, if any.
*/
-    if (has_skip_array && beyond_end_advance &&
+    if (has_skip_array && !all_required_satisfied &&
!has_required_opposite_direction_skip && pstate->finaltup)
pstate->beyondskip = true;

However, a small number of my test cases now fail. And (I assume) this
approach has certain downsides on leaf pages where we're now too quick
to stop maintaining skip arrays.

Since I've built with the above change and executed make check, I found
that there is an assertion error, which may not be related to what you
pointed
out.

* the reproducible simple query (you can see the original query in
opr_sanity.sql).
select * from pg_proc
where proname in (
'lo_lseek64',
'lo_truncate',
'lo_truncate64')
and pronamespace = 11;

* the assertion error
TRAP: failed Assert("sktrig_required && required"), File:
"nbtutils.c", Line: 3375, PID: 362411

While investigating the error, I thought we might need to consider
whether key->sk_flags does not have SK_BT_SKIP. The assertion error
occurs because
requiredSameDir doesn't become true since proname does not have
SK_BT_SKIP.

+		if (beyondskip)
+		{
+			/*
+			 * "Beyond end advancement" skip scan optimization.
+			 *
+			 * Just skip over any skip array scan keys.  Treat all other scan
+			 * keys as not required for the scan to continue.
+			 */
+			Assert(!prechecked);
+
+			if (key->sk_flags & SK_BT_SKIP)
+				continue;
+		}
+		else if (((key->sk_flags & SK_BT_REQFWD) && 
ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && 
ScanDirectionIsBackward(dir)))
requiredSameDir = true;

My previous investigation was incorrect, sorry. IIUC,
_bt_check_compare()
should return false as soon as possible with continuescan=true after the
tuple fails the key check when beyondskip=true, rather than setting
requiredSameDir to true. Because it has already been triggered to
perform
a full index scan for the page.

Though the change fixes the assertion error in 'make check', there are
still
cases where the number of return values is incorrect. I would also like
to
continue investigating.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

In reply to: Masahiro Ikeda (#51)
4 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Nov 22, 2024 at 4:17 AM Masahiro Ikeda <ikedamsh@oss.nttdata.com> wrote:

Though the change fixes the assertion error in 'make check', there are
still
cases where the number of return values is incorrect. I would also like
to
continue investigating.

Thanks for taking a look at that! I've come up with a better approach,
though (sorry about how quickly this keeps changing!). See the
attached new revision, v17.

I'm now calling the new optimization from the third patch the
"skipskip" optimization. I believe that v17 will fix those bugs that
you saw -- let me know if those are gone now. It also greatly improves
the situation with regressions (more on that later).

New approach
------------

Before now, I was trying to preserve the usual invariants that we have
for the scan's array keys: the array keys must "track the progress" of
the scan through the index's key space -- that's what those
_bt_tuple_before_array_skeys precondition and postcondition asserts
inside _bt_advance_array_keys verify for us (these assertions have
proven very valuable, both now and during the earlier Postgres 17
nbtree SAOP project). My original approach (to managing the overhead
of maintaining skip arrays in adversarial/unsympathetic cases) was
overly complicated, inflexible, and brittle.

It's simpler (much simpler) to allow the scan to temporarily forget
about the invariants: once the "skipskip" optimization is activated,
we don't care about the rules that require that "the array keys track
progress through the key space" -- not until _bt_readpage actually
reaches the page's finaltup. Then _bt_readpage can handle the problem
using a trick that we already use elsewhere (e.g., in btrestrpos):
_bt_readpage just calls _bt_start_array_keys to get the array keys to
their initial positions (in the current scan direction), before
calling _bt_checkkeys for finaltup in the usual way.

Under this scheme, the _bt_checkkeys call for finaltup will definitely
call _bt_advance_array_keys to advance the array keys to the correct
place (the correct place when scanning forward is ">= finaltup in the
key space"). The truly essential thing is that we never accidentally
allow the array keys to "prematurely get ahead of the scan's tuple in
the keyspace" -- that leads to wrong answers to queries once we reach
the next page. But the arrays can be allowed to temporarily remain
*before* the scan's tuples without consequence (it's safe when the
skipskip optimization is in effect, at least, since the _bt_checkkeys
calls treat everything as a non-required key, and so
_bt_checkkeys/_bt_advance_array_keys will never expect any non-skipped
SAOP arrays to tells them anything about the scan's progress through
the index's key space -- there'll be no unsafe "cur_elem_trig" binary
searches, for example).

This approach also allowed me to restore all of the assertions in
nbtutils.c to their original form. That was important to me -- those
assertions have saved me quite a few times.

Regressions, performance improvements
-------------------------------------

As I touched on briefly already, the new approach is also
significantly *faster* than the master branch in certain "adversarial"
cases (this is explained in the next paragraph). Overall, all of the
cases that were unacceptably regressed before now, that I know of
(e.g., all of the cases that you brought to my attention so far,
Masahiro) are either significantly faster, or only very slightly
slower. The regressions that we still have in v17 are probably
acceptable -- though clearly I still have a lot of performance
validation work to do before reaching a final conclusion about
regressions.

I also attach a variant of your test_for_v15.sql test case, Masahiro.
I used this to do some basic performance validation of this latest
version of the patch -- it'll give you a general sense of where we are
with regressions, and will also show those "adversarial" cases that
end up significantly faster than the master branch with v17.
Significant improvements are sometimes seen because the "skipskip"
optimization replaces the requiredOppositeDirOnly optimization (see
commit e0b1ee17dc for context). We can do better than the existing
requiredOppositeDirOnly optimizations because we can skip more
individual scan key comparisons. For example, with this query:

SELECT * FROM t WHERE id1 BETWEEN 0 AND 1_000_000 AND id2 = 1_000_000

This goes from taking ~25ms with a warm cache on master, to only
taking ~17ms on v17 of the patch series. I wasn't really trying to
make anything faster, here -- it's a nice bonus.

There are 3 scan keys involved here, when the query is run on the
master branch: "id1 >= 0", "id1 <= 1_000_000", and "id2 = 1_000_000".
The existing requiredOppositeDirOnly optimization doesn't work because
only one page will ever have its pstate->first set to 'true' within
_bt_readpage -- that's due to "id2 = 1_000_000" (a non-required scan
key) only rarely evaluating to true. Whereas with skip scan (and its
"skipskip" optimization), there are only 2 scan keys (well, sort of):
the range skip array scan key on "id1", plus the "id2 = 1_000_000"
scan key. We'll be able to skip over the range skip array scan key
entirely with the "skipskip" optimization, so the range skip array's
lower_bound and upper_bound "subsidiary" scan keys won't need to be
evaluated more than once per affected leaf page. In other words, we'll
still need to evaluate the "id2 = 1_000_000" against every tuple on
every leaf page -- but we don't need to use the >= or <=
subsidiary-to-skip-array scan keys (except once per page, to establish
that the optimization is safe).

--
Peter Geoghegan

Attachments:

v17-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v17-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From 9cabfc5675fcb01fe6f4c077d6561cfb7d12ed99 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v17 1/3] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 contrib/bloom/blscan.c                        |   1 +
 doc/src/sgml/bloom.sgml                       |   6 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 22 files changed, 243 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index e1884acf4..7b4180db5 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -153,6 +153,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index c0b978119..52939d317 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -585,6 +585,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index f2fd62afb..5e423e155 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index b35b8a975..36f1435cb 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index 0d99d6abc..927ba1039 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 60c61039d..ec259012b 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index dd76fe1da..8bbb3d734 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -69,6 +69,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -552,6 +553,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -578,6 +580,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -705,6 +708,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			Assert(btscan->btps_nextScanPage != P_NONE);
 			*next_scan_page = btscan->btps_nextScanPage;
@@ -805,6 +813,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -839,6 +849,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 0cd046613..82bb93d1a 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -970,6 +970,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 301786185..be668abf2 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a3f1d53d7..5838948af 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2098,6 +2100,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2111,6 +2114,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2127,6 +2131,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2645,6 +2650,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index 0c5fb725e..f77f716f0 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -116,6 +116,7 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++)
 	{
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 19f2b172c..92b13f539 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -170,9 +170,10 @@ CREATE INDEX
    Heap Blocks: exact=28
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..1792.00 rows=2 width=0) (actual time=0.356..0.356 rows=29 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 0.408 ms
-(8 rows)
+(9 rows)
 </programlisting>
   </para>
 
@@ -202,11 +203,12 @@ CREATE INDEX
    -&gt;  BitmapAnd  (cost=24.34..24.34 rows=2 width=0) (actual time=0.027..0.027 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..12.04 rows=500 width=0) (actual time=0.026..0.026 rows=0 loops=1)
                Index Cond: (i5 = 123451)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
-(9 rows)
+(10 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 840d7f816..c68eb770e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4198,12 +4198,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index cd12b9ce4..482715397 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -727,8 +727,10 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Heap Blocks: exact=10
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 1
  Planning Time: 0.485 ms
  Execution Time: 0.073 ms
 </screen>
@@ -779,6 +781,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Heap Blocks: exact=90
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
  Planning Time: 0.187 ms
  Execution Time: 3.036 ms
 </screen>
@@ -844,6 +847,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -873,9 +877,11 @@ EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique
          Buffers: shared hit=4 read=3
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared hit=2
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
                Buffers: shared hit=2 read=3
  Planning:
    Buffers: shared hit=3
@@ -908,6 +914,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Heap Blocks: exact=90
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1042,6 +1049,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
  Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
  Planning Time: 0.077 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a854..e042638b7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -502,9 +502,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    Batches: 1  Memory Usage: 24kB
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(7 rows)
+(8 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index ae9ce9d8e..c24d56007 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index f6b8329cd..a8bc0bfd7 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7a03b4e36..e3e99272a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2657,47 +2661,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off)
@@ -2713,16 +2726,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2741,7 +2757,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off)
@@ -2757,16 +2773,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2787,7 +2806,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2877,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2897,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,17 +2986,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2982,17 +3013,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3064,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3048,17 +3091,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3112,17 +3161,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3144,17 +3199,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3482,12 +3543,14 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(15);
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3566,10 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(25);
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3617,17 @@ explain (analyze, costs off, summary off, timing off) select * from ma_test wher
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4197,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 33a6dceb0..b7cf35b9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index 2eaeb1477..9afe205e0 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 442428d93..085e746af 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

v17-0002-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v17-0002-Add-skip-scan-to-nbtree.patchDownload
From 5727024e8a84c059a8a265e149d97eeef959d248 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v17 2/3] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
useful in cases where the total number of distinct values in the column
'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.

The design of skip arrays allows array advancement to work without
concern for which specific array types are involved; only the lowest
level code needs to treat skip arrays and SAOP arrays differently.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values are conceptually just like any
other value that might appear in either kind of array.  A call made to
_bt_first to reposition the scan based on a NEXT/PRIOR sentinel usually
won't locate a leaf page containing tuples that must be returned to the
scan, but that's normal during any skip scan that happens to involve
"sparse" indexed values (skip support can only help with "dense" indexed
values, which isn't meaningfully possible when the skipped column is of
a continuous type, where skip support typically can't be implemented).

The optimizer doesn't use distinct new index paths to represent index
skip scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  Skipping is possible during
eligible bitmap index scans, index scans, and index-only scans.  It's
also possible during eligible parallel scans.  An eligible scan is any
scan that nbtree preprocessing generates a skip array for, which happens
whenever doing so will enable later preprocessing to mark original input
scan keys (passed by the executor) as required to continue the scan.

There are hardly any limitations around where skip arrays/scan keys may
appear relative to conventional/input scan keys.  This is no less true
in the presence of conventional SAOP array scan keys, which may both
roll over and be rolled over by skip arrays.  For example, a skip array
on the column "b" is generated for "WHERE a = 42 AND c IN (5, 6, 7, 8)".
As always (since commit 5bf748b8), whether or not nbtree actually skips
depends in large part on physical index characteristics at runtime.
Preprocessing is optimistic about skipping working out: it applies
simple static rules to decide where to place skip arrays.  It's up to
array-related scan code to keep the overhead of maintaining the scan's
skip arrays under control when it turns out that skipping isn't helpful.

Preprocessing will never cons up a skip array for an index column that
has an equality strategy scan key on input, but will do so for indexed
columns that are only constrained by inequality strategy scan keys.
This allows the scan to skip using a range skip array.  These are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   28 +-
 src/include/catalog/pg_amproc.dat             |   22 +
 src/include/catalog/pg_proc.dat               |   27 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  101 +
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  273 +++
 src/backend/access/nbtree/nbtree.c            |  213 ++-
 src/backend/access/nbtree/nbtsearch.c         |   75 +-
 src/backend/access/nbtree/nbtutils.c          | 1695 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   46 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  379 +++-
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/timestamp.c             |   48 +
 src/backend/utils/adt/uuid.c                  |   71 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/bloom.sgml                       |    1 +
 doc/src/sgml/btree.sgml                       |   34 +-
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   49 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |   10 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   61 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    5 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 36 files changed, 2976 insertions(+), 333 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c51de742e..0826c124f 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -202,7 +202,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 123fba624..d841e85bc 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1022,10 +1024,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* skip scan support (unless !use_sksup) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo   *low_order;		/* low_compare's ORDER proc */
+	FmgrInfo   *high_order;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1113,6 +1127,10 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on omitted prefix column */
+#define SK_BT_NEGPOSINF	0x00080000	/* -inf/+inf key (invalid sk_argument) */
+#define SK_BT_NEXT		0x00100000	/* positions scan > sk_argument */
+#define SK_BT_PRIOR		0x00200000	/* positions scan < sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1149,6 +1167,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1159,7 +1181,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5d7fe292b..c5af25806 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cbbe8acd3..abfe92666 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2260,6 +2275,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4447,6 +4465,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6290,6 +6311,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9327,6 +9351,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d70e6d37e..5b58739ad 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..d97e6059d
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,101 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining pairs of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code falls back on next-key sentinel values for any opclass that
+ * doesn't provide its own skip support function.  There is no benefit to
+ * providing skip support for an opclass on a type where guessing that the
+ * next indexed value is the next possible indexable value seldom works out.
+ * It might not even be feasible to add skip support for a continuous type.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order).
+	 * This gives the B-Tree code a useful value to start from, before any
+	 * data has been read from the index.  These are also sometimes used to
+	 * detect when the scan has run out of indexable values to search for.
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 *
+	 * Operator class decrement/increment functions never have to directly
+	 * deal with the value NULL, nor with ASC vs. DESC column ordering.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 1859be614..b4f1466d4 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..26ebbf776 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 8bbb3d734..105bcfe2f 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -29,6 +29,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -70,7 +71,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -78,11 +79,23 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -535,10 +548,159 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume all input scankeys will be output with its own
+	 * SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * scan array (and associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		Form_pg_attribute attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array/skip scan key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array/skip scan key, while freeing memory allocated
+		 * for old sk_argument where required
+		 */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -549,7 +711,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -572,16 +735,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -608,6 +771,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -652,7 +816,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -674,14 +838,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -719,7 +878,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -763,11 +922,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -806,7 +965,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -815,7 +974,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -833,6 +992,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -842,7 +1002,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -852,14 +1012,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 82bb93d1a..43e321896 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -984,6 +984,13 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  Skip
+	 * array keys are = keys, though we'll sometimes need to treat the key as
+	 * if it was some kind of inequality instead.  This is required whenever
+	 * the skip array's current array element is a sentinel value.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1059,8 +1066,39 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is -inf or +inf, choose low_compare/high_compare
+				 * (or consider if they imply a NOT NULL constraint).
+				 */
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skiparraysk = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					if (!array->null_elem)
+						impliesNN = skiparraysk;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1097,6 +1135,39 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					strat_total == BTLessStrategyNumber)
 					break;
 
+				/*
+				 * If this is a scan key for a skip array whose current
+				 * element is marked NEXT or PRIOR, adjust strat_total
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(strat_total == BTEqualStrategyNumber);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[].
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).
+					 */
+					break;
+				}
+
 				/*
 				 * Done if that was the last attribute, or if next key is not
 				 * in sequence (implying no boundary key is available for the
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 896696ff7..9de7c40e8 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	Oid			eq_op;			/* = op to be used to add array, if any */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -63,16 +91,46 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
+static int	_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+										  int *numSkipArrayKeys);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_decrement(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_increment(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -256,6 +314,12 @@ _bt_freestack(BTStack stack)
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -271,10 +335,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -282,30 +348,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_preprocess_num_array_keys(scan, skipatts,
+												 &numSkipArrayKeys);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys described within skipatts[]
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -320,17 +380,18 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/* Process input keys, and emit skip array keys described by skipatts[] */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -346,19 +407,97 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and its scan key where indicated */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* attribute already had an "=" key on input */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[numArrayKeyData];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].low_order = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].high_order = NULL;	/* for now */
+
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how this
+			 * constructed "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			numArrayKeyData++;	/* keep this scan key/array */
+		}
+
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
+		cur = &arrayKeyData[numArrayKeyData];
 		*cur = scan->keyData[input_ikey];
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -414,7 +553,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -425,7 +564,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -442,7 +581,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -517,23 +656,234 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].low_order = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].high_order = NULL;	/* unused */
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
 	return arrayKeyData;
 }
 
+/*
+ *	_bt_preprocess_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit all required so->arrayKeys[] entries.
+ *
+ * Also sets *numSkipArrayKeys to the number of skip arrays that caller must
+ * make sure to add to the scan keys it'll output (complete with corresponding
+ * so->arrayKeys[] entries), and sets the corresponding attributes positions
+ * in *skipatts argument.  Every attribute that receives a skip array will
+ * have its skip support routine set in *skipatts, too.
+ */
+static int
+_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+							  int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_inkey = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				(*numSkipArrayKeys)++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				break;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatt
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatt->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										 BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack even an equality strategy
+	 * operator, but they're supported.  We cope by making caller not use a
+	 * skip array for this index column (nor for any lower-order columns).
+	 */
+	if (!OidIsValid(skipatt->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatt->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+													   reverse,
+													   &skipatt->sksup);
+
+	/* might not have set up skip support, but can skip either way */
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys_final() -- fix up array scan key references
  *
@@ -633,7 +983,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -669,6 +1020,14 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && !array->null_elem)
+						_bt_skip_preproc_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
@@ -684,7 +1043,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -980,26 +1339,28 @@ _bt_merge_arrays(IndexScanDesc scan, ScanKey skey, FmgrInfo *sortproc,
  * guaranteeing that at least the scalar scan key can be considered redundant.
  * We return false if the comparison could not be made (caller must keep both
  * scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar inequality scan keys.
  */
 static bool
 _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
 	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
 		   skey->sk_strategy != BTEqualStrategyNumber);
@@ -1009,8 +1370,7 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1041,11 +1401,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensures that the scalar scan key can be eliminated as redundant
+		 */
+		eliminated = true;
+
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+	}
+	else
+	{
+		/*
+		 * With a skip array, it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when there's a lack of suitable cross-type support for
+		 * comparing skey to an existing inequality associated with the array.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when
+ * determining its redundancy against a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1097,10 +1511,298 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of a skip array scan key when determining its
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array.  This forces the array's elements to be generated within the limits
+ * of a range that's described/constrained by the array's inequalities.
+ *
+ * Return value indicates if the scalar scan key could be "eliminated".  We
+ * return true in the common case where caller's scan key was successfully
+ * rolled into the skip array.  We return false when we can't do that due to
+ * the presence of an existing, conflicting inequality that cannot be compared
+ * to caller's inequality due to a lack of suitable cross-type support.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way scan code can generally assume that
+	 * it'll always be safe to use the ORDER procs stored in so->orderProcs[].
+	 * The only exception is cases where the array's scan key is NEGPOSINF.
+	 *
+	 * When the array's scan key is marked NEGPOSINF, then it'll lack a valid
+	 * sk_argument.  The scan must apply the array's low_compare and/or
+	 * high_compare (if any) to establish whether a given tupdatum is within
+	 * the range of the skip array.  This could involve applying the 3-way
+	 * low_order/high_order ORDER proc we store here (though never the ORDER
+	 * proc stored in so->orderProcs[]).
+	 *
+	 * Note: we sometimes use low_compare/high_compare inequalities with the
+	 * array's current element value, and with values that were mutated by the
+	 * array's skip support routine.  A skip array must always use its index
+	 * attribute's own input opclass/type, so this will always work correctly.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (!array->high_compare)
+			{
+				/* array currently lacks a high_compare, make space for one */
+				array->high_compare = palloc(sizeof(ScanKeyData));
+				array->high_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare, keep old one */
+
+				/* replace old high_compare with caller's new one */
+			}
+
+			/* caller's high_compare becomes skip array's high_compare */
+			*array->high_compare = *skey;
+			*array->high_order = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (!array->low_compare)
+			{
+				/* array currently lacks a low_compare, make space for one */
+				array->low_compare = palloc(sizeof(ScanKeyData));
+				array->low_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare, keep old one */
+
+				/* replace old low_compare with caller's new one */
+			}
+
+			/* caller's low_compare becomes skip array's low_compare */
+			*array->low_compare = *skey;
+			*array->low_order = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
 
+/*
+ * Perform final preprocessing for a skip array.  Called when the skip array's
+ * final low_compare and high_compare have been chosen.
+ *
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts the
+ * operator strategies).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value NEGPOSINF, using >= instead of using >
+ * (or using <= instead of < during backwards scans) makes it safe to include
+ * lower-order scan keys in the insertion scan key (there must be lower-order
+ * scan keys after the skip array).  We will avoid an extra _bt_first to find
+ * the first value in the index > sk_argument, at least when the first real
+ * matching value in the index happens to be an exact match for the newly
+ * transformed (newly incremented/decremented) sk_argument value.
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01' -- which would be wrong.
+ */
+static void
+_bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+	Assert(!array->null_elem);
+
+	/* If skip array doesn't have skip support, can't apply optimization */
+	if (!array->use_sksup)
+		return;
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skip_preproc_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skip_preproc_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skip_preproc_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+	Assert(array->use_sksup);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type.
+	 *
+	 * Note: we have to support the convention that sk_subtype == InvalidOid
+	 * means the opclass input type.
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup.decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skip_preproc_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+	Assert(array->use_sksup);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type.
+	 *
+	 * Note: we have to support the convention that sk_subtype == InvalidOid
+	 * means the opclass input type.
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup.increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  * qsort_arg comparator for sorting array elements
  */
@@ -1220,6 +1922,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1342,6 +2046,98 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, because the array doesn't really
+ * have any elements (it generates its array elements procedurally instead).
+ * Note that this may include a NULL value/an IS NULL qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ *
+ * Note: This function doesn't actually binary search.  It uses an interface
+ * that's as similar to _bt_binsrch_array_skey as possible for consistency.
+ * "Binary searching a skip array" just means determining if tupdatum/tupnull
+ * are before, within, or beyond the range of caller's skip array.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1351,29 +2147,480 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting skip array to lowest element, which isn't just NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Setting skip array to highest element, which isn't just NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element is out of bounds for the array.
+		 *
+		 * This is only possible if low_compare represents an >= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the prior value cannot possibly satisfy low_compare, so we can
+		 * give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(array->low_order,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Decrement by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &uflow);
+	if (uflow)
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the bounds of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element is out of bounds for the array.
+		 *
+		 * This is only possible if high_compare represents an <= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the next value cannot possibly satisfy high_compare, so we can
+		 * give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(array->high_order,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Increment by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &oflow);
+	if (oflow)
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the bounds of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1389,6 +2636,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1398,29 +2646,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1480,6 +2729,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1487,7 +2737,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1499,16 +2748,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1633,9 +2876,93 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_NEGPOSINF))
+		{
+			/* Scankey has a valid/comparable sk_argument value (not -inf) */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXT))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument + infinitesimal"
+				 */
+				if (ScanDirectionIsForward(dir))
+				{
+					/* tupdatum is still < "sk_argument + infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was forward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to backward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+			else if (result == 0 && (cur->sk_flags & SK_BT_PRIOR))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument - infinitesimal"
+				 */
+				if (ScanDirectionIsBackward(dir))
+				{
+					/* tupdatum is still > "sk_argument - infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was backward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to forward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum be its current element.
+				 */
+				return false;
+			}
+		}
+		else
+		{
+			/*
+			 * Scankey searches for the sentinel value -inf (or +inf).
+			 *
+			 * Note: -inf could mean "true negative infinity", or it could
+			 * just represent the lowest/first value that satisfies the skip
+			 * array's low_compare.  +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * "Compare tupdatum against -inf" using array's low_compare, if
+			 * any (or "compare it against +inf" using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is >= -inf and <= +inf.  It's time for caller to
+				 * advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1967,18 +3294,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -2003,18 +3321,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2032,12 +3341,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -2113,11 +3431,62 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  "Binary searching" only determined whether
+		 * tupdatum is beyond, before, or within the range of the skip array.
+		 *
+		 * As always, we set the array element to its closest available match.
+		 * But unlike with a conventional array, a skip array's new element
+		 * might be NEGPOSINF, which represents the lowest (or the highest)
+		 * real value that is within the range of the skip array.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == "some array element".
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2467,6 +3836,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2479,10 +3850,24 @@ end_toplevel_scan:
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never consed up skip array scan keys, it would be possible for "gaps"
+ * to appear that made it unsafe to mark any subsequent input scan keys (taken
+ * from scan->keyData[]) as required to continue the scan.  If there are no
+ * keys for a given attribute, the keys for subsequent attributes can never be
+ * required.  For instance, "WHERE y = 4" always required a full-index scan
+ * prior to Postgres 18.  Typically, preprocessing is now able to "rewrite"
+ * such a qual into "WHERE x = ANY('{every possible x value}') and y = 4".
+ * The addition of a consed up skip array key on "x" enables marking the key
+ * on "y" (as well as the one on "x") required, according to the usual rules.
+ * Of course, this transformation process cannot change the tuples returned by
+ * the scan -- the skip array will use an IS NULL qual when searching for its
+ * "NULL element" (in this particular example, at least).  But skip arrays can
+ * make the scan much more efficient: if there are few distinct values in "x",
+ * we'll be able to skip over many irrelevant leaf pages.  (If on the other
+ * hand there are many distinct values in "x" then the scan will degenerate
+ * into a full index scan at run time.)
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -2519,7 +3904,12 @@ end_toplevel_scan:
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -2583,6 +3973,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -2671,7 +4069,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -2728,7 +4127,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -2776,6 +4174,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * Typically, our call to _bt_preprocess_array_keys will have
+			 * already added "=" skip array keys as required to form an
+			 * unbroken series of "=" constraints on all attrs prior to the
+			 * attr from the last scan key that came from our scan->keyData[]
+			 * input scan keys.  However, certain implementation restrictions
+			 * might have prevented array preprocessing from doing that for us
+			 * (e.g., opclasses without an "=" operator can't use skip scan).
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -2864,6 +4270,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -2872,6 +4279,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -2891,8 +4299,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -3034,10 +4440,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3103,6 +4510,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -3112,6 +4522,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3185,6 +4611,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3746,6 +5173,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index db6ed784a..86a5de8f4 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 16144c2b7..2cef9816e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -370,6 +370,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 85e5eaf32..88c929a0a 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -98,6 +98,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 8130f3e8a..256350007 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f73f294b8..cb8470324 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -85,6 +85,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 08fa6774d..42fde7ef2 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -192,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -213,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5736,6 +5740,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6794,6 +6884,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6803,17 +6940,22 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6829,14 +6971,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -6846,13 +6993,90 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't absord a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6874,6 +7098,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6894,7 +7119,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6910,6 +7135,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6925,6 +7182,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -7033,104 +7291,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index e00cd3c96..213a6326f 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2286,6 +2287,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 5284d23dc..654bda638 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -384,6 +387,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 8a67f0120..6befee3a4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1751,6 +1752,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3590,6 +3602,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 92b13f539..6fa688769 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -206,6 +206,7 @@ CREATE INDEX
                Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
+               Index Searches: 0
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
 (10 rows)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index d3358dfc3..fa6f27e6d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1938,7 +1938,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -1958,7 +1958,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 9b2973694..4c4909691 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4440,24 +4440,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -7562,19 +7563,23 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                        QUERY PLAN                         
------------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Nested Loop
    ->  Hash Join
          Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-         ->  Seq Scan on fkest f2
-               Filter: (x100 = 2)
+         ->  Bitmap Heap Scan on fkest f2
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
          ->  Hash
-               ->  Seq Scan on fkest f1
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f1
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
          Index Cond: (x = f1.x)
-(10 rows)
+(14 rows)
 
 alter table fkest add constraint fk
   foreign key (x, x10b, x100) references fkest (x, x10, x100);
@@ -7583,20 +7588,24 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                     QUERY PLAN                      
------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Hash Join
    Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
    ->  Hash Join
          Hash Cond: (f3.x = f2.x)
          ->  Seq Scan on fkest f3
          ->  Hash
-               ->  Seq Scan on fkest f2
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f2
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Hash
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-(11 rows)
+         ->  Bitmap Heap Scan on fkest f1
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
+(15 rows)
 
 rollback;
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 36dc31c16..aef239200 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5240,9 +5240,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index c73631a9a..62eb4239a 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1461,18 +1461,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index fe162cc7c..dea40e013 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -766,7 +766,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -776,7 +776,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 08521d51a..89f021ccd 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2666,6 +2667,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

v17-0003-Add-skipskip-nbtree-skip-scan-optimization.patchapplication/octet-stream; name=v17-0003-Add-skipskip-nbtree-skip-scan-optimization.patchDownload
From 3ad6aa3dbdf79847da7c78367b88d81bbe87cf39 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v17 3/3] Add "skipskip" nbtree skip scan optimization.

Fix regressions in cases that are nominally eligible to use skip scan
but can never actually benefit from skipping.  These are cases where the
leading skipped prefix column contains many distinct values -- often as
many distinct values are there are total index tuples.

For example, the test case posted here is fixed by the work from this
commit:

https://postgr.es/m/51d00219180323d121572e1f83ccde2a@oss.nttdata.com

Note that this commit doesn't actually change anything about when or how
skip scan decides when or how to skip.  It just avoids wasting CPU
cycles on uselessly maintaining a skip array at the tuple granularity,
preferring to maintain the skip arrays at something closer to the page
granularity when that makes sense.  See:

https://www.postgresql.org/message-id/flat/CAH2-Wz%3DE7XrkvscBN0U6V81NK3Q-dQOmivvbEsjG-zwEfDdFpg%40mail.gmail.com#7d34e8aa875d7a718043834c5ef4c167

Doing well on cases like this is important because we can't expect the
optimizer to never choose an affected plan -- we prefer to solve these
problems in the executor, which has access to the most reliable and
current information about the index.  The optimizer can afford to be
very optimistic about skipping if actual runtime scan behavior is very
similar to a traditional full index scan in the worst case.  See
"optimizer" section from the original intro mail for more information:

https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP%2BG4bw%40mail.gmail.com

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h           |   7 +
 src/backend/access/nbtree/nbtsearch.c |  20 +++
 src/backend/access/nbtree/nbtutils.c  | 191 +++++++++++++++++++++++---
 3 files changed, 197 insertions(+), 21 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index d841e85bc..c84dcc983 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1111,6 +1111,13 @@ typedef struct BTReadPageState
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
 
+	/*
+	 * Input and output parameters, set and unset by both _bt_readpage and
+	 * _bt_checkkeys to manage "skipskip" optimization during skip scans
+	 */
+	bool		skipskip;
+	bool		noskipskip;
+
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
 	 * (only used during scans with array keys)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 43e321896..468bfb722 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1644,6 +1644,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.continuescan = true; /* default assumption */
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
+	pstate.skipskip = false;
+	pstate.noskipskip = false;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
@@ -1686,6 +1688,10 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * if the final tuple is == those same keys (and also satisfies any
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
+	 *
+	 * XXX Maybe we should opt out of this optimization during any skip scan?
+	 * Arguably, it is superseded by the "skipskip" skip scan optimization.
+	 * (In any case this isn't very effective during most skip scans.)
 	 */
 	if (!firstPage && !so->scanBehind && minoff < maxoff)
 	{
@@ -1837,6 +1843,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
+			if (pstate.skipskip)
+			{
+				Assert(itup == pstate.finaltup);
+
+				_bt_start_array_keys(scan, dir);
+				pstate.skipskip = false;	/* reset for finaltup */
+			}
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -1897,6 +1910,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff && pstate.skipskip)
+			{
+				Assert(itup == pstate.finaltup);
+
+				_bt_start_array_keys(scan, dir);
+				pstate.skipskip = false;	/* reset for finaltup */
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 9de7c40e8..0a3a3bf32 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -151,11 +151,14 @@ static bool _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption);
 static void _bt_mark_scankey_required(ScanKey skey);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool skipskip,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
 								 ScanDirection dir, bool *continuescan);
+static bool _bt_checkkeys_is_skipskip_safe(IndexScanDesc scan, BTReadPageState *pstate,
+										   IndexTuple tuple, TupleDesc tupdesc);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -3135,6 +3138,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	ScanDirection dir = so->currPos.dir;
 	int			arrayidx = 0;
 	bool		beyond_end_advance = false,
+				has_skip_array = false,
 				has_required_opposite_direction_only = false,
 				oppodir_inequality_sktrig = false,
 				all_required_satisfied = true,
@@ -3189,6 +3193,8 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			{
 				array = &so->arrayKeys[arrayidx++];
 				Assert(array->scan_key == ikey);
+				if (array->num_elems == -1)
+					has_skip_array = true;
 			}
 		}
 		else
@@ -3211,8 +3217,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -3359,7 +3363,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -3401,7 +3405,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -3416,7 +3420,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -3528,7 +3532,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -3597,6 +3601,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		return false;
 	}
 
+	/* _bt_readpage must have unset skipskip flag (for finaltup call) */
+	Assert(!pstate->skipskip);
+
 	/*
 	 * Postcondition array state assertion (for still-unsatisfied tuples).
 	 *
@@ -3766,6 +3773,25 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		pstate->skip = pstate->maxoff + 1;
 	}
 
+	/*
+	 * Optimization: if a scan with a skip array doesn't satisfy every
+	 * required key (in practice this is almost always all the scan's keys),
+	 * we assume that this page isn't likely to skip "within" a page using
+	 * _bt_checkkeys_look_ahead.  We'll apply the 'skipskip' optimization.
+	 *
+	 * The 'skipskip' optimization allows _bt_checkkeys/_bt_check_compare to
+	 * stop maintaining the scan's skip arrays until we've reached finaltup.
+	 */
+	else if (has_skip_array && !pstate->noskipskip &&
+			 !all_required_satisfied && tuple != pstate->finaltup)
+	{
+		/* Don't consider 'skipskip' optimization more than once per page */
+		if (_bt_checkkeys_is_skipskip_safe(scan, pstate, tuple, tupdesc))
+			pstate->skipskip = true;
+		else
+			pstate->noskipskip = true;
+	}
+
 	/* Caller's tuple doesn't match the new qual */
 	return false;
 
@@ -3867,7 +3893,8 @@ end_toplevel_scan:
  * make the scan much more efficient: if there are few distinct values in "x",
  * we'll be able to skip over many irrelevant leaf pages.  (If on the other
  * hand there are many distinct values in "x" then the scan will degenerate
- * into a full index scan at run time.)
+ * into a full index scan at run time, but we'll be no worse off overall.
+ * _bt_checkkeys's 'skipskip' optimization keeps the runtime overhead low.)
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -4908,7 +4935,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+							arrayKeys, pstate->skipskip,
+							pstate->prechecked, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
@@ -4920,7 +4948,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
 		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
+		Assert(!pstate->skipskip && !pstate->prechecked &&
+			   !pstate->firstmatch);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
@@ -4934,7 +4963,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, pstate->skipskip, false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -5065,7 +5094,7 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	Assert(so->numArrayKeys);
 
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -5104,17 +5133,24 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Though we advance non-required array keys on our own, that shouldn't have
  * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
+ * have no fixed relationship with the scan's progress.  (skipskip=true makes
+ * us treat required scan keys as non-required, though.  That's safe because
+ * _bt_readpage will account for our failure to fully maintain the scan's
+ * arrays later on, right before its finaltup call to _bt_checkkeys.)
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass skipskip=true to instruct us to skip all of the scan's skip arrays.
+ * This provides the scan with a way of keeping the cost of maintaining its
+ * skip arrays under control, given skip arrays on high cardinality columns
+ * (i.e. given a skip array on a column which isn't a good fit for skip scan).
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool skipskip,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -5131,10 +5167,18 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when reading a page that's now using the "skipskip" optimization)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (skipskip)
+		{
+			Assert(!prechecked);
+
+			if (key->sk_flags & SK_BT_SKIP)
+				continue;
+		}
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
@@ -5247,7 +5291,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * forward scans.)
 				 */
 				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
-					ScanDirectionIsBackward(dir))
+					!skipskip && ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
 			else
@@ -5265,7 +5309,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * backward scans.)
 				 */
 				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
-					ScanDirectionIsForward(dir))
+					!skipskip && ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
 
@@ -5499,6 +5543,100 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 	return result;
 }
 
+/*
+ * Can _bt_checkkeys/_bt_check_compare apply the 'skipskip' optimization?
+ *
+ * Called when _bt_advance_array_keys finds that no required scan key is
+ * satisfied during a scan with at least one skip array.
+ *
+ * Return value indicates if the optimization is safe for the tuples on the
+ * page after caller's tuple, but before its page's finaltup.
+ */
+static bool
+_bt_checkkeys_is_skipskip_safe(IndexScanDesc scan, BTReadPageState *pstate,
+							   IndexTuple tuple, TupleDesc tupdesc)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	Relation	rel = scan->indexRelation;
+	ScanDirection dir = so->currPos.dir;
+	IndexTuple	finaltup = pstate->finaltup;
+	int			arrayidx = 0,
+				nfinaltupatts;
+	bool		rangearrayseen = false;
+
+	Assert(!BTreeTupleIsPivot(tuple));
+
+	/*
+	 * Don't attempt the optimization when reading the rightmost leaf page
+	 * (unless scanning backwards, when it's the leftmost leaf page instead)
+	 */
+	if (!finaltup)
+		return false;
+
+	nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+	for (int ikey = 0; ikey < so->numberOfKeys; ikey++)
+	{
+		ScanKey		cur = so->keyData + ikey;
+		BTArrayKeyInfo *array = NULL;
+		Datum		tupdatum;
+		bool		tupnull;
+		int32		result;
+
+		/*
+		 * Only need to check range skip arrays within this loop.
+		 *
+		 * A SAOP array can always be treated as a non-required array within
+		 * _bt_check_compare.  A skip array without a lower or upper bound is
+		 * always safe to skip within _bt_check_compare, since it is satisfied
+		 * by every possible value.
+		 */
+		if (cur->sk_strategy != BTEqualStrategyNumber)
+			continue;
+		if (!(cur->sk_flags & SK_SEARCHARRAY))
+			continue;
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == ikey);
+		if (array->num_elems != -1 || array->null_elem)
+			continue;
+
+		/*
+		 * Found a range skip array to test.
+		 *
+		 * Scans with more than one range skip array are not eligible to use
+		 * the optimization.  Note that we support the skipskip optimization
+		 * for a qual like "WHERE a BETWEEN 1 AND 10 AND b BETWEEN 1 AND 3",
+		 * since there the qual actually requires only a single skip array.
+		 * However, if such a qual ended with "... AND C > 42", then it will
+		 * prevent use of the skipskip optimization. (XXX we could probably be
+		 * smarter than this...but is it worth it?)
+		 */
+		if (rangearrayseen)
+			return false;
+		rangearrayseen = true;
+
+		/* test the tuple that just advanced arrays within our caller */
+		Assert(cur->sk_flags & SK_BT_SKIP);
+		Assert(cur->sk_flags & SK_BT_REQFWD);
+		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], false, dir,
+								   tupdatum, tupnull, array, cur, &result);
+		if (result != 0)
+			return false;
+
+		/* test the page's finaltup iff relevant attribute isn't truncated */
+		if (cur->sk_attno > nfinaltupatts)
+			continue;
+
+		tupdatum = index_getattr(finaltup, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], false, dir,
+								   tupdatum, tupnull, array, cur, &result);
+		if (result != 0)
+			return false;
+	}
+
+	return true;
+}
+
 /*
  * Determine if a scan with array keys should skip over uninteresting tuples.
  *
@@ -5524,6 +5662,17 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	/*
+	 * Use of the "look ahead" skipping mechanism is mutually exclusive with
+	 * use of skip scan's similar "skipskip" mechanism
+	 *
+	 * XXX While it's true that we can only use one of the optimization at a
+	 * time, there's still nothing stopping us from trying this one out first.
+	 * Maybe _bt_checkkeys_is_skipskip_safe should be taught to apply a test
+	 * against pstate->rechecks, so as to give this optimization a chance?
+	 */
+	Assert(!pstate->skipskip);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
-- 
2.45.2

test_for_v17.sqlapplication/octet-stream; name=test_for_v17.sqlDownload
#53Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Peter Geoghegan (#52)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 2024-11-23 07:34, Peter Geoghegan wrote:

On Fri, Nov 22, 2024 at 4:17 AM Masahiro Ikeda
<ikedamsh@oss.nttdata.com> wrote:

Though the change fixes the assertion error in 'make check', there are
still
cases where the number of return values is incorrect. I would also
like
to
continue investigating.

Thanks for taking a look at that! I've come up with a better approach,
though (sorry about how quickly this keeps changing!). See the
attached new revision, v17.

I'm now calling the new optimization from the third patch the
"skipskip" optimization. I believe that v17 will fix those bugs that
you saw -- let me know if those are gone now. It also greatly improves
the situation with regressions (more on that later).

New approach
------------

Before now, I was trying to preserve the usual invariants that we have
for the scan's array keys: the array keys must "track the progress" of
the scan through the index's key space -- that's what those
_bt_tuple_before_array_skeys precondition and postcondition asserts
inside _bt_advance_array_keys verify for us (these assertions have
proven very valuable, both now and during the earlier Postgres 17
nbtree SAOP project). My original approach (to managing the overhead
of maintaining skip arrays in adversarial/unsympathetic cases) was
overly complicated, inflexible, and brittle.

It's simpler (much simpler) to allow the scan to temporarily forget
about the invariants: once the "skipskip" optimization is activated,
we don't care about the rules that require that "the array keys track
progress through the key space" -- not until _bt_readpage actually
reaches the page's finaltup. Then _bt_readpage can handle the problem
using a trick that we already use elsewhere (e.g., in btrestrpos):
_bt_readpage just calls _bt_start_array_keys to get the array keys to
their initial positions (in the current scan direction), before
calling _bt_checkkeys for finaltup in the usual way.

Under this scheme, the _bt_checkkeys call for finaltup will definitely
call _bt_advance_array_keys to advance the array keys to the correct
place (the correct place when scanning forward is ">= finaltup in the
key space"). The truly essential thing is that we never accidentally
allow the array keys to "prematurely get ahead of the scan's tuple in
the keyspace" -- that leads to wrong answers to queries once we reach
the next page. But the arrays can be allowed to temporarily remain
*before* the scan's tuples without consequence (it's safe when the
skipskip optimization is in effect, at least, since the _bt_checkkeys
calls treat everything as a non-required key, and so
_bt_checkkeys/_bt_advance_array_keys will never expect any non-skipped
SAOP arrays to tells them anything about the scan's progress through
the index's key space -- there'll be no unsafe "cur_elem_trig" binary
searches, for example).

This approach also allowed me to restore all of the assertions in
nbtutils.c to their original form. That was important to me -- those
assertions have saved me quite a few times.

Regressions, performance improvements
-------------------------------------

As I touched on briefly already, the new approach is also
significantly *faster* than the master branch in certain "adversarial"
cases (this is explained in the next paragraph). Overall, all of the
cases that were unacceptably regressed before now, that I know of
(e.g., all of the cases that you brought to my attention so far,
Masahiro) are either significantly faster, or only very slightly
slower. The regressions that we still have in v17 are probably
acceptable -- though clearly I still have a lot of performance
validation work to do before reaching a final conclusion about
regressions.

I also attach a variant of your test_for_v15.sql test case, Masahiro.
I used this to do some basic performance validation of this latest
version of the patch -- it'll give you a general sense of where we are
with regressions, and will also show those "adversarial" cases that
end up significantly faster than the master branch with v17.
Significant improvements are sometimes seen because the "skipskip"
optimization replaces the requiredOppositeDirOnly optimization (see
commit e0b1ee17dc for context). We can do better than the existing
requiredOppositeDirOnly optimizations because we can skip more
individual scan key comparisons. For example, with this query:

SELECT * FROM t WHERE id1 BETWEEN 0 AND 1_000_000 AND id2 = 1_000_000

This goes from taking ~25ms with a warm cache on master, to only
taking ~17ms on v17 of the patch series. I wasn't really trying to
make anything faster, here -- it's a nice bonus.

There are 3 scan keys involved here, when the query is run on the
master branch: "id1 >= 0", "id1 <= 1_000_000", and "id2 = 1_000_000".
The existing requiredOppositeDirOnly optimization doesn't work because
only one page will ever have its pstate->first set to 'true' within
_bt_readpage -- that's due to "id2 = 1_000_000" (a non-required scan
key) only rarely evaluating to true. Whereas with skip scan (and its
"skipskip" optimization), there are only 2 scan keys (well, sort of):
the range skip array scan key on "id1", plus the "id2 = 1_000_000"
scan key. We'll be able to skip over the range skip array scan key
entirely with the "skipskip" optimization, so the range skip array's
lower_bound and upper_bound "subsidiary" scan keys won't need to be
evaluated more than once per affected leaf page. In other words, we'll
still need to evaluate the "id2 = 1_000_000" against every tuple on
every leaf page -- but we don't need to use the >= or <=
subsidiary-to-skip-array scan keys (except once per page, to establish
that the optimization is safe).

Thanks for updating the patch!

The approach looks good to me. In fact, the significant regressions I
reported have disappeared in my environment. And the test_for_v17.sql
shows that the performance of the master and the master with your
patches
is almost same.

One thing I am concerned about is that it reduces the cases where the
optimization of _bt_checkkeys_look_ahead() works effectively, as the
approach
skips the skip immediately on the first occurrence per page. But, I'd
like to
take your approach because I prioritize stability.

FWIW, I conducted tests to understand the downside of 0003 patch. IIUC,
the worst-case scenario occurs when the first few tuples per page have
different values for the first attribute, while the rest are the same
value. The result
shows that the 0003 patch caused a 2x degradation in performance,
although the
performance is faster than that of the master branch.

* master with 0001, 0002 and 0003 patch.
-- SET skipscan_prefix_cols=32
Index Only Scan using t_idx on public.t (cost=0.42..3576.58 rows=2712
width=8) (actual time=0.019..15.107 rows=2717 loops=1)
Output: id1, id2
Index Cond: (t.id2 = 360)
Index Searches: 2696
Heap Fetches: 0
Buffers: shared hit=8126
Settings: effective_cache_size = '7932MB', work_mem = '15MB'
Planning Time: 0.049 ms
Execution Time: 15.203 ms
(9 rows)

* master with 0001 and 0002 patch. (without 0003 patch)
-- SET skipscan_prefix_cols=32
Index Only Scan using t_idx on public.t (cost=0.42..3576.55 rows=2709
width=8) (actual time=
0.011..6.886 rows=2717 loops=1)
Output: id1, id2
Index Cond: (t.id2 = 360)
Index Searches: 2696
Heap Fetches: 0
Buffers: shared hit=8126
Settings: effective_cache_size = '7932MB', work_mem = '15MB'
Planning Time: 0.062 ms
Execution Time: 6.981 ms
(9 rows)

* the way to get the above result.
-- create table
DROP TABLE IF EXISTS t;
CREATE unlogged TABLE t (id1 int, id2 int);
-- A special case where the 0003 patch performs poorly.
-- It inserts 367 index tuples per page, making only the first two id1
-- values different, and then makes the rest the same.
-- psql=# SELECT * FROM bt_page_stats('t_idx', 1);
-- -[ RECORD 1 ]-+-----
-- blkno | 1
-- type | l
-- live_items | 367
-- dead_items | 0
-- avg_item_size | 16
-- page_size | 8192
-- free_size | 808
-- btpo_prev | 0
-- btpo_next | 2
-- btpo_level | 0
-- btpo_flags | 1
INSERT INTO t (
SELECT CASE WHEN i%368<3 THEN i%368+i/368*100 ELSE i/368*1000+10 END,
i%368
FROM generate_series(1, 1_000_000) s(i)
);
CREATE INDEX t_idx on t (id1, id2);
VACUUM FREEZE ANALYZE;

-- example data
SELECT * FROM t LIMIT 10;
SELECT * FROM t WHERE id2 > 365 LIMIT 10;

-- performance
SET skipscan_prefix_cols=32;
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT, SETTINGS, WAL, VERBOSE) SELECT *
FROM t WHERE id2 = 360; -- cache
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT, SETTINGS, WAL, VERBOSE) SELECT *
FROM t WHERE id2 = 360;
SET skipscan_prefix_cols=0;
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT, SETTINGS, WAL, VERBOSE) SELECT *
FROM t WHERE id2 = 360;

Again, the above results are provided for reference, as I believe that
many users prioritize stability and I'd like to take your new approach.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

In reply to: Masahiro Ikeda (#53)
3 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Mon, Nov 25, 2024 at 4:07 AM Masahiro Ikeda <ikedamsh@oss.nttdata.com> wrote:

The approach looks good to me. In fact, the significant regressions I
reported have disappeared in my environment. And the test_for_v17.sql
shows that the performance of the master and the master with your
patches
is almost same.

That's great.

I'm also a bit worried about regressing queries that don't even
involve skip arrays -- particularly very simple queries. I'm thinking
of things like "pgbench -S" style SELECT queries. Adding almost any
new code to a hot code path such as _bt_check_compare comes with a
risk of such regressions. (Up until now it has been convenient to
pretend that "skipscan_prefix_cols=0" is 100% representative of
master, but obviously it is less than 100% representative -- since
"skipscan_prefix_cols=0" still has enlarged object code size in places
like _bt_check_compare.)

Attached is v18, which makes _bt_check_compare copy all relevant scan
key fields into local variables. This gives the compiler more freedom
due to not having to worry about aliasing.

I believe that this change to _bt_check_compare fixes some regressions
with simple queries. This includes a "pgbench -S" variant that I
previously used during the Postgres 17 SAOP array work -- a script
that showed a favorable case for that 17 work, involving "SELECT aid
FROM pgbench_accounts WHERE aid IN (....)", with hundreds of
contiguous array elements. More importantly, it avoids regressions
with standard "pgbench -S" SELECT queries.

It takes a long time to validate changes such as these -- the
regressions are within the range of noise, so it is difficult to tell
the difference between signal and noise. I probably went further with
some of the new changes to _bt_check_compare than really made sense.
For example, I probably added likely() or unlikely() annotations that
don't really help. This is something that I will continue to refine.

One thing I am concerned about is that it reduces the cases where the
optimization of _bt_checkkeys_look_ahead() works effectively, as the
approach
skips the skip immediately on the first occurrence per page.

I noticed that with the recent v17 revision of the patch, my original
MDAM paper "sales_mdam_paper" test case (the complicated query in the
introductory email of this thread) was about 2x slower. That's just
not okay, obviously. But the issue was relatively easy to fix: it was
fixed by making _bt_readpage not apply the "skipskip" optimization
when on the first page for the current primitive index scan -- we
already do this with the "precheck" optimization, so it's natural to
do it with the "skipskip" optimization as well.

The "sales_mdam_paper" test case involves thousands of primitive index
scans that each access only one leaf page. But each leaf page returns
2 non-adjoining tuples, with quite a few non-matching tuples "in
between" the matching tuples. There is one matching tuple for "store =
200", and another for "store = 250" -- and there's non-matching stores
201 - 249 between these two, which we want _bt_checkkeys_look_ahead to
skip over. This is exactly the kind of case where the
_bt_checkkeys_look_ahead() optimization is expected to help.

-- performance
SET skipscan_prefix_cols=32;
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT, SETTINGS, WAL, VERBOSE) SELECT *
FROM t WHERE id2 = 360; -- cache
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT, SETTINGS, WAL, VERBOSE) SELECT *
FROM t WHERE id2 = 360;
SET skipscan_prefix_cols=0;
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT, SETTINGS, WAL, VERBOSE) SELECT *
FROM t WHERE id2 = 360;

Again, the above results are provided for reference, as I believe that
many users prioritize stability and I'd like to take your new approach.

Adversarial cases specifically designed to "make the patch look bad"
are definitely useful review feedback. Ideally, the patch will be 100%
free of regressions -- no matter how unlikely (or even silly) they may
seem. I always prefer to not have to rely on anybody's opinion of what
is likely or unlikely. :-)

A quick test seems to show that this particular regression is more or
less fixed by v18. As you said, the _bt_checkkeys_look_ahead stuff is
the issue here (same with the MDAM sales query). You should confirm
that the issue has actually been fixed, though.

Thanks
--
Peter Geoghegan

Attachments:

v18-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v18-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From a6ca239ba9dda7c19614aef06c449daa4f11351e Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v18 1/3] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 contrib/bloom/blscan.c                        |   1 +
 doc/src/sgml/bloom.sgml                       |   6 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 22 files changed, 243 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index e1884acf4..7b4180db5 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -153,6 +153,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index c0b978119..52939d317 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -585,6 +585,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index f2fd62afb..5e423e155 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index b35b8a975..36f1435cb 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index 0d99d6abc..927ba1039 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 60c61039d..ec259012b 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index dd76fe1da..8bbb3d734 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -69,6 +69,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -552,6 +553,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -578,6 +580,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -705,6 +708,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			Assert(btscan->btps_nextScanPage != P_NONE);
 			*next_scan_page = btscan->btps_nextScanPage;
@@ -805,6 +813,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -839,6 +849,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 0cd046613..82bb93d1a 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -970,6 +970,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 301786185..be668abf2 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a3f1d53d7..5838948af 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2098,6 +2100,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2111,6 +2114,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2127,6 +2131,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2645,6 +2650,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index 0c5fb725e..f77f716f0 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -116,6 +116,7 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++)
 	{
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 19f2b172c..92b13f539 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -170,9 +170,10 @@ CREATE INDEX
    Heap Blocks: exact=28
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..1792.00 rows=2 width=0) (actual time=0.356..0.356 rows=29 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 0.408 ms
-(8 rows)
+(9 rows)
 </programlisting>
   </para>
 
@@ -202,11 +203,12 @@ CREATE INDEX
    -&gt;  BitmapAnd  (cost=24.34..24.34 rows=2 width=0) (actual time=0.027..0.027 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..12.04 rows=500 width=0) (actual time=0.026..0.026 rows=0 loops=1)
                Index Cond: (i5 = 123451)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
-(9 rows)
+(10 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 840d7f816..c68eb770e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4198,12 +4198,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index cd12b9ce4..482715397 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -727,8 +727,10 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Heap Blocks: exact=10
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 1
  Planning Time: 0.485 ms
  Execution Time: 0.073 ms
 </screen>
@@ -779,6 +781,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Heap Blocks: exact=90
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
  Planning Time: 0.187 ms
  Execution Time: 3.036 ms
 </screen>
@@ -844,6 +847,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -873,9 +877,11 @@ EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique
          Buffers: shared hit=4 read=3
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared hit=2
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
                Buffers: shared hit=2 read=3
  Planning:
    Buffers: shared hit=3
@@ -908,6 +914,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Heap Blocks: exact=90
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1042,6 +1049,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
  Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
  Planning Time: 0.077 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a854..e042638b7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -502,9 +502,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    Batches: 1  Memory Usage: 24kB
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(7 rows)
+(8 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index ae9ce9d8e..c24d56007 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index f6b8329cd..a8bc0bfd7 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7a03b4e36..e3e99272a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2657,47 +2661,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off)
@@ -2713,16 +2726,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2741,7 +2757,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off)
@@ -2757,16 +2773,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2787,7 +2806,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2877,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2897,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,17 +2986,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2982,17 +3013,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3064,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3048,17 +3091,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3112,17 +3161,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3144,17 +3199,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3482,12 +3543,14 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(15);
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3566,10 @@ explain (analyze, costs off, summary off, timing off) execute mt_q1(25);
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3617,17 @@ explain (analyze, costs off, summary off, timing off) select * from ma_test wher
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4197,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 33a6dceb0..b7cf35b9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index 2eaeb1477..9afe205e0 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 442428d93..085e746af 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

v18-0003-Add-skipskip-nbtree-skip-scan-optimization.patchapplication/octet-stream; name=v18-0003-Add-skipskip-nbtree-skip-scan-optimization.patchDownload
From 099f34335abbd95904fa23d85b8d4513a1975792 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v18 3/3] Add "skipskip" nbtree skip scan optimization.

Fix regressions in cases that are nominally eligible to use skip scan
but can never actually benefit from skipping.  These are cases where the
leading skipped prefix column contains many distinct values -- often as
many distinct values are there are total index tuples.

For example, the test case posted here is fixed by the work from this
commit:

https://postgr.es/m/51d00219180323d121572e1f83ccde2a@oss.nttdata.com

Note that this commit doesn't actually change anything about when or how
skip scan decides when or how to skip.  It just avoids wasting CPU
cycles on uselessly maintaining a skip array at the tuple granularity,
preferring to maintain the skip arrays at something closer to the page
granularity when that makes sense.  See:

https://www.postgresql.org/message-id/flat/CAH2-Wz%3DE7XrkvscBN0U6V81NK3Q-dQOmivvbEsjG-zwEfDdFpg%40mail.gmail.com#7d34e8aa875d7a718043834c5ef4c167

Doing well on cases like this is important because we can't expect the
optimizer to never choose an affected plan -- we prefer to solve these
problems in the executor, which has access to the most reliable and
current information about the index.  The optimizer can afford to be
very optimistic about skipping if actual runtime scan behavior is very
similar to a traditional full index scan in the worst case.  See
"optimizer" section from the original intro mail for more information:

https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP%2BG4bw%40mail.gmail.com

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h           |   8 +
 src/backend/access/nbtree/nbtree.c    |   1 +
 src/backend/access/nbtree/nbtsearch.c |  34 +++-
 src/backend/access/nbtree/nbtutils.c  | 222 +++++++++++++++++++++-----
 4 files changed, 222 insertions(+), 43 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index d841e85bc..fd5de8181 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1051,6 +1051,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Last array advancement matched -inf attr? */
 	bool		oppositeDirCheck;	/* explicit scanBehind recheck needed? */
@@ -1111,6 +1112,13 @@ typedef struct BTReadPageState
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
 
+	/*
+	 * Input and output parameters, set and unset by both _bt_readpage and
+	 * _bt_checkkeys to manage "skipskip" optimization during skip scans
+	 */
+	bool		skipskip;
+	bool		noskipskip;
+
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
 	 * (only used during scans with array keys)
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 923629d5e..bf239f811 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -423,6 +423,7 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 		memcpy(scan->keyData, scankey, scan->numberOfKeys * sizeof(ScanKeyData));
 	so->numberOfKeys = 0;		/* until _bt_preprocess_keys sets it */
 	so->numArrayKeys = 0;		/* ditto */
+	so->skipScan = false;		/* ditto */
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 43e321896..db000b8f6 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1644,6 +1644,26 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.continuescan = true; /* default assumption */
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
+
+	/*
+	 * Initialize "skipskip" optimization state (used only during scans with
+	 * skip arrays).
+	 *
+	 * Skip scans use this to manage the overhead of maintaining skip arrays
+	 * on columns with many distinct values.  It also works as a substitute
+	 * for the pstate.prechecked optimization, which skip scan never uses.
+	 *
+	 * We never do this for the first page read by each primitive scan.  This
+	 * avoids slowing down queries with skip arrays that have relatively few
+	 * distinct values -- the "look ahead" optimization is preferred there.
+	 */
+	pstate.skipskip = false;
+	pstate.noskipskip = firstPage;
+
+	/*
+	 * Initialize "look ahead" optimization state (used only during scans with
+	 * arrays, including those that just use skip arrays)
+	 */
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
@@ -1660,10 +1680,10 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * corresponding value from the last item on the page.  So checking with
 	 * the last item on the page would give a more precise answer.
 	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
+	 * We don't do this for the first page read by each (primitive) scan, to
+	 * avoid slowing down point queries.  They typically don't stand to gain
+	 * much when the optimization can be applied, and are more likely to
+	 * notice the overhead of the precheck.  Also avoid it during skip scans.
 	 *
 	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
 	 * just set a low-order required array's key to the best available match
@@ -1687,7 +1707,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!firstPage && !so->scanBehind && minoff < maxoff)
+	if (!firstPage && !so->skipScan && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
@@ -1837,6 +1857,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
+			if (pstate.skipskip)
+				pstate.skipskip = false;	/* reset for finaltup */
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -1897,6 +1919,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff && pstate.skipskip)
+				pstate.skipskip = false;	/* reset for finaltup */
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 12905e799..1f7ae5f7a 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -151,11 +151,14 @@ static bool _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption);
 static void _bt_mark_scankey_required(ScanKey skey);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool skipskip,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
 								 ScanDirection dir, bool *continuescan);
+static bool _bt_checkkeys_skipskip(IndexScanDesc scan, IndexTuple tuple,
+								   IndexTuple finaltup, TupleDesc tupdesc);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -364,6 +367,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	 * is the size of the original scan->keyData[] input array, plus space for
 	 * any additional skip array scan keys described within skipatts[]
 	 */
+	so->skipScan = (numSkipArrayKeys > 0);
 	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
 
 	/*
@@ -2883,7 +2887,7 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 											tupdatum, tupnull,
 											cur->sk_argument, cur);
 
-			if (result == 0 && (cur->sk_flags & SK_BT_NEXT))
+			if (unlikely(result == 0 && (cur->sk_flags & SK_BT_NEXT)))
 			{
 				/*
 				 * tupdatum is == sk_argument, but true current array element
@@ -2903,7 +2907,7 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 				 */
 				return false;
 			}
-			else if (result == 0 && (cur->sk_flags & SK_BT_PRIOR))
+			else if (unlikely(result == 0 && (cur->sk_flags & SK_BT_PRIOR)))
 			{
 				/*
 				 * tupdatum is == sk_argument, but true current array element
@@ -3211,8 +3215,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -3359,7 +3361,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -3401,7 +3403,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -3416,7 +3418,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -3528,7 +3530,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -3597,6 +3599,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		return false;
 	}
 
+	/* _bt_readpage must have unset skipskip flag (for finaltup call) */
+	Assert(!pstate->skipskip);
+
 	/*
 	 * Postcondition array state assertion (for still-unsatisfied tuples).
 	 *
@@ -3766,6 +3771,25 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		pstate->skip = pstate->maxoff + 1;
 	}
 
+	/*
+	 * Optimization: if a scan with a skip array doesn't satisfy every
+	 * required key (in practice this is almost always all the scan's keys),
+	 * we assume that this page isn't likely to skip "within" a page using
+	 * _bt_checkkeys_look_ahead.  We'll apply the 'skipskip' optimization.
+	 *
+	 * The 'skipskip' optimization allows _bt_checkkeys/_bt_check_compare to
+	 * stop maintaining the scan's skip arrays until we've reached finaltup.
+	 */
+	else if (so->skipScan && !pstate->noskipskip && !all_required_satisfied &&
+			 tuple != pstate->finaltup)
+	{
+		/* Don't consider 'skipskip' optimization more than once per page */
+		if (_bt_checkkeys_skipskip(scan, tuple, pstate->finaltup, tupdesc))
+			pstate->skipskip = true;
+		else
+			pstate->noskipskip = true;
+	}
+
 	/* Caller's tuple doesn't match the new qual */
 	return false;
 
@@ -3867,7 +3891,8 @@ end_toplevel_scan:
  * make the scan much more efficient: if there are few distinct values in "x",
  * we'll be able to skip over many irrelevant leaf pages.  (If on the other
  * hand there are many distinct values in "x" then the scan will degenerate
- * into a full index scan at run time.)
+ * into a full index scan at run time, but we'll be no worse off overall.
+ * _bt_checkkeys's 'skipskip' optimization keeps the runtime overhead low.)
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -4908,7 +4933,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+							arrayKeys, pstate->skipskip,
+							pstate->prechecked, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
@@ -4920,7 +4946,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
 		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
+		Assert(!pstate->skipskip && !pstate->prechecked &&
+			   !pstate->firstmatch);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
@@ -4934,7 +4961,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, pstate->skipskip, false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -5065,7 +5092,7 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	Assert(so->numArrayKeys);
 
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -5104,17 +5131,24 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Though we advance non-required array keys on our own, that shouldn't have
  * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
+ * have no fixed relationship with the scan's progress.  (skipskip=true makes
+ * us treat required scan keys as non-required, though.  That's safe because
+ * _bt_readpage will account for our failure to fully maintain the scan's
+ * arrays later on, right before its finaltup call to _bt_checkkeys.)
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass skipskip=true to instruct us to skip all of the scan's skip arrays.
+ * This provides the scan with a way of keeping the cost of maintaining its
+ * skip arrays under control, given skip arrays on high cardinality columns
+ * (i.e. given a skip array on a column which isn't a good fit for skip scan).
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool skipskip,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -5124,6 +5158,10 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 	for (; *ikey < so->numberOfKeys; (*ikey)++)
 	{
 		ScanKey		key = so->keyData + *ikey;
+		int			sk_flags = key->sk_flags;
+		Oid			sk_collation = key->sk_collation;
+		FmgrInfo	sk_func = key->sk_func;
+		Datum		sk_argument = key->sk_argument;
 		Datum		datum;
 		bool		isNull;
 		bool		requiredSameDir = false,
@@ -5131,13 +5169,21 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when reading a page that's now using the "skipskip" optimization)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (unlikely(skipskip))
+		{
+			Assert(!prechecked);
+
+			if (sk_flags & SK_BT_SKIP)
+				continue;
+		}
+		else if (((sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
-		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
-				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
+		else if (((sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
+				 ((sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
 			requiredOppositeDirOnly = true;
 
 		/*
@@ -5158,7 +5204,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		 */
 		if (prechecked &&
 			(requiredSameDir || (requiredOppositeDirOnly && firstmatch)) &&
-			!(key->sk_flags & SK_ROW_HEADER))
+			!(sk_flags & SK_ROW_HEADER))
 			continue;
 
 		if (key->sk_attno > tupnatts)
@@ -5174,7 +5220,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		}
 
 		/* row-comparison keys need special processing */
-		if (key->sk_flags & SK_ROW_HEADER)
+		if (sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
 									 continuescan))
@@ -5191,28 +5237,27 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		 * A skip array scan key might be negative/positive infinity.  Might
 		 * also be next key/prior key sentinel, which we don't deal with.
 		 */
-		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR))
+		if (unlikely(sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)))
 		{
-			Assert(key->sk_flags & SK_SEARCHARRAY);
-			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(sk_flags & SK_SEARCHARRAY);
+			Assert(sk_flags & SK_BT_SKIP);
 			Assert(requiredSameDir);
 
 			*continuescan = false;
 			return false;
 		}
 
-
-		if (key->sk_flags & SK_ISNULL)
+		if (sk_flags & SK_ISNULL)
 		{
 			/* Handle IS NULL/NOT NULL tests */
-			if (key->sk_flags & SK_SEARCHNULL)
+			if (sk_flags & SK_SEARCHNULL)
 			{
 				if (isNull)
 					continue;	/* tuple satisfies this qual */
 			}
 			else
 			{
-				Assert(key->sk_flags & SK_SEARCHNOTNULL);
+				Assert(sk_flags & SK_SEARCHNOTNULL);
 				if (!isNull)
 					continue;	/* tuple satisfies this qual */
 			}
@@ -5233,7 +5278,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		if (isNull)
 		{
-			if (key->sk_flags & SK_BT_NULLS_FIRST)
+			if (sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -5247,7 +5292,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -5265,7 +5310,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -5284,8 +5329,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		 * an earlier tuple from this same page satisfied it earlier on.
 		 */
 		if (!(requiredOppositeDirOnly && firstmatch) &&
-			!DatumGetBool(FunctionCall2Coll(&key->sk_func, key->sk_collation,
-											datum, key->sk_argument)))
+			!DatumGetBool(FunctionCall2Coll(&sk_func, sk_collation,
+											datum, sk_argument)))
 		{
 			/*
 			 * Tuple fails this qual.  If it's a required qual for the current
@@ -5308,7 +5353,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			 */
 			else if (advancenonrequired &&
 					 key->sk_strategy == BTEqualStrategyNumber &&
-					 (key->sk_flags & SK_SEARCHARRAY))
+					 (sk_flags & SK_SEARCHARRAY))
 				return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
 											  tupdesc, *ikey, false);
 
@@ -5500,6 +5545,100 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 	return result;
 }
 
+/*
+ * Can _bt_checkkeys/_bt_check_compare apply the 'skipskip' optimization?
+ *
+ * Called when _bt_advance_array_keys finds that no required scan key is
+ * satisfied during a scan with at least one skip array.
+ *
+ * Return value indicates if the optimization is safe for the tuples on the
+ * page after caller's tuple, but before its page's finaltup.
+ */
+static bool
+_bt_checkkeys_skipskip(IndexScanDesc scan, IndexTuple tuple,
+					   IndexTuple finaltup, TupleDesc tupdesc)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	Relation	rel = scan->indexRelation;
+	ScanDirection dir = so->currPos.dir;
+	int			arrayidx = 0,
+				nfinaltupatts = 0;
+	bool		rangearrayseen = false;
+
+	Assert(!BTreeTupleIsPivot(tuple));
+
+	if (finaltup)
+		nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+	for (int ikey = 0; ikey < so->numberOfKeys; ikey++)
+	{
+		ScanKey		cur = so->keyData + ikey;
+		BTArrayKeyInfo *array = NULL;
+		Datum		tupdatum;
+		bool		tupnull;
+		int32		result;
+
+		/*
+		 * Only need to check range skip arrays within this loop.
+		 *
+		 * A SAOP array can always be treated as a non-required array within
+		 * _bt_check_compare.  A skip array without a lower or upper bound is
+		 * always safe to skip within _bt_check_compare, since it is satisfied
+		 * by every possible value.
+		 */
+		if (cur->sk_strategy != BTEqualStrategyNumber)
+			continue;
+		if (!(cur->sk_flags & SK_SEARCHARRAY))
+			continue;
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == ikey);
+		if (array->num_elems != -1 || array->null_elem)
+			continue;
+
+		/*
+		 * Found a range skip array to test.
+		 *
+		 * Scans with more than one range skip array are not eligible to use
+		 * the optimization.  Note that we support the skipskip optimization
+		 * for a qual like "WHERE a BETWEEN 1 AND 10 AND b BETWEEN 1 AND 3",
+		 * since there the qual actually requires only a single skip array.
+		 * However, if such a qual ended with "... AND C > 42", then it will
+		 * prevent use of the skipskip optimization.
+		 */
+		if (rangearrayseen)
+			return false;
+
+		/*
+		 * Don't attempt the optimization when we have a skip array and are
+		 * reading the rightmost leaf page (or the leftmost leaf page, when
+		 * scanning backwards)
+		 */
+		if (!finaltup)
+			return false;
+		rangearrayseen = true;
+
+		/* test the tuple that just advanced arrays within our caller */
+		Assert(cur->sk_flags & SK_BT_SKIP);
+		Assert(cur->sk_flags & SK_BT_REQFWD);
+		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], false, dir,
+								   tupdatum, tupnull, array, cur, &result);
+		if (result != 0)
+			return false;
+
+		/* test the page's finaltup iff relevant attribute isn't truncated */
+		if (cur->sk_attno > nfinaltupatts)
+			continue;
+
+		tupdatum = index_getattr(finaltup, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], false, dir,
+								   tupdatum, tupnull, array, cur, &result);
+		if (result != 0)
+			return false;
+	}
+
+	return true;
+}
+
 /*
  * Determine if a scan with array keys should skip over uninteresting tuples.
  *
@@ -5525,6 +5664,13 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	/*
+	 * The "look ahead" skipping mechanism cannot be used at the same time as
+	 * skip scan's similar "skipskip" mechanism (though it can be used during
+	 * skip scans when it's not in use)
+	 */
+	Assert(!pstate->skipskip);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
-- 
2.45.2

v18-0002-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v18-0002-Add-skip-scan-to-nbtree.patchDownload
From aa5517864fc569d9756116ba08cbf6c1be9e680e Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v18 2/3] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
useful in cases where the total number of distinct values in the column
'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.

The design of skip arrays allows array advancement to work without
concern for which specific array types are involved; only the lowest
level code needs to treat skip arrays and SAOP arrays differently.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values are conceptually just like any
other value that might appear in either kind of array.  A call made to
_bt_first to reposition the scan based on a NEXT/PRIOR sentinel usually
won't locate a leaf page containing tuples that must be returned to the
scan, but that's normal during any skip scan that happens to involve
"sparse" indexed values (skip support can only help with "dense" indexed
values, which isn't meaningfully possible when the skipped column is of
a continuous type, where skip support typically can't be implemented).

The optimizer doesn't use distinct new index paths to represent index
skip scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  Skipping is possible during
eligible bitmap index scans, index scans, and index-only scans.  It's
also possible during eligible parallel scans.  An eligible scan is any
scan that nbtree preprocessing generates a skip array for, which happens
whenever doing so will enable later preprocessing to mark original input
scan keys (passed by the executor) as required to continue the scan.

There are hardly any limitations around where skip arrays/scan keys may
appear relative to conventional/input scan keys.  This is no less true
in the presence of conventional SAOP array scan keys, which may both
roll over and be rolled over by skip arrays.  For example, a skip array
on the column "b" is generated for "WHERE a = 42 AND c IN (5, 6, 7, 8)".
As always (since commit 5bf748b8), whether or not nbtree actually skips
depends in large part on physical index characteristics at runtime.
Preprocessing is optimistic about skipping working out: it applies
simple static rules to decide where to place skip arrays.  It's up to
array-related scan code to keep the overhead of maintaining the scan's
skip arrays under control when it turns out that skipping isn't helpful.

Preprocessing will never cons up a skip array for an index column that
has an equality strategy scan key on input, but will do so for indexed
columns that are only constrained by inequality strategy scan keys.
This allows the scan to skip using a range skip array.  These are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   28 +-
 src/include/catalog/pg_amproc.dat             |   22 +
 src/include/catalog/pg_proc.dat               |   27 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  101 +
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  273 +++
 src/backend/access/nbtree/nbtree.c            |  214 ++-
 src/backend/access/nbtree/nbtsearch.c         |   75 +-
 src/backend/access/nbtree/nbtutils.c          | 1696 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   46 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  379 +++-
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/timestamp.c             |   48 +
 src/backend/utils/adt/uuid.c                  |   71 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/bloom.sgml                       |    1 +
 doc/src/sgml/btree.sgml                       |   34 +-
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   49 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |   10 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   61 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    5 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 36 files changed, 2978 insertions(+), 333 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c51de742e..0826c124f 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -202,7 +202,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 123fba624..d841e85bc 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1022,10 +1024,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* skip scan support (unless !use_sksup) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo   *low_order;		/* low_compare's ORDER proc */
+	FmgrInfo   *high_order;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1113,6 +1127,10 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on omitted prefix column */
+#define SK_BT_NEGPOSINF	0x00080000	/* -inf/+inf key (invalid sk_argument) */
+#define SK_BT_NEXT		0x00100000	/* positions scan > sk_argument */
+#define SK_BT_PRIOR		0x00200000	/* positions scan < sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1149,6 +1167,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1159,7 +1181,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5d7fe292b..c5af25806 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cbbe8acd3..abfe92666 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2260,6 +2275,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4447,6 +4465,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6290,6 +6311,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9327,6 +9351,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d70e6d37e..5b58739ad 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..d97e6059d
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,101 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining pairs of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code falls back on next-key sentinel values for any opclass that
+ * doesn't provide its own skip support function.  There is no benefit to
+ * providing skip support for an opclass on a type where guessing that the
+ * next indexed value is the next possible indexable value seldom works out.
+ * It might not even be feasible to add skip support for a continuous type.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order).
+	 * This gives the B-Tree code a useful value to start from, before any
+	 * data has been read from the index.  These are also sometimes used to
+	 * detect when the scan has run out of indexable values to search for.
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 *
+	 * Operator class decrement/increment functions never have to directly
+	 * deal with the value NULL, nor with ASC vs. DESC column ordering.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 1859be614..b4f1466d4 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..26ebbf776 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 8bbb3d734..923629d5e 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -29,6 +29,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -70,7 +71,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -78,11 +79,24 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 *
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -535,10 +549,159 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume all input scankeys will be output with its own
+	 * SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * scan array (and associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		Form_pg_attribute attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array/skip scan key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array/skip scan key, while freeing memory allocated
+		 * for old sk_argument where required
+		 */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -549,7 +712,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -572,16 +736,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -608,6 +772,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -652,7 +817,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -674,14 +839,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -719,7 +879,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -763,11 +923,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -806,7 +966,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -815,7 +975,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -833,6 +993,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -842,7 +1003,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer((void *) parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -852,14 +1013,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 82bb93d1a..43e321896 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -984,6 +984,13 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  Skip
+	 * array keys are = keys, though we'll sometimes need to treat the key as
+	 * if it was some kind of inequality instead.  This is required whenever
+	 * the skip array's current array element is a sentinel value.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1059,8 +1066,39 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is -inf or +inf, choose low_compare/high_compare
+				 * (or consider if they imply a NOT NULL constraint).
+				 */
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skiparraysk = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					if (!array->null_elem)
+						impliesNN = skiparraysk;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1097,6 +1135,39 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					strat_total == BTLessStrategyNumber)
 					break;
 
+				/*
+				 * If this is a scan key for a skip array whose current
+				 * element is marked NEXT or PRIOR, adjust strat_total
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(strat_total == BTEqualStrategyNumber);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[].
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).
+					 */
+					break;
+				}
+
 				/*
 				 * Done if that was the last attribute, or if next key is not
 				 * in sequence (implying no boundary key is available for the
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 896696ff7..12905e799 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	Oid			eq_op;			/* = op to be used to add array, if any */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -63,16 +91,46 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
+static int	_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+										  int *numSkipArrayKeys);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_decrement(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_increment(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -256,6 +314,12 @@ _bt_freestack(BTStack stack)
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -271,10 +335,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -282,30 +348,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_preprocess_num_array_keys(scan, skipatts,
+												 &numSkipArrayKeys);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys described within skipatts[]
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -320,17 +380,18 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/* Process input keys, and emit skip array keys described by skipatts[] */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -346,19 +407,97 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and its scan key where indicated */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* attribute already had an "=" key on input */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[numArrayKeyData];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].low_order = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].high_order = NULL;	/* for now */
+
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how this
+			 * constructed "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			numArrayKeyData++;	/* keep this scan key/array */
+		}
+
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
+		cur = &arrayKeyData[numArrayKeyData];
 		*cur = scan->keyData[input_ikey];
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -414,7 +553,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -425,7 +564,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -442,7 +581,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -517,23 +656,234 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].low_order = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].high_order = NULL;	/* unused */
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
 	return arrayKeyData;
 }
 
+/*
+ *	_bt_preprocess_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit all required so->arrayKeys[] entries.
+ *
+ * Also sets *numSkipArrayKeys to the number of skip arrays that caller must
+ * make sure to add to the scan keys it'll output (complete with corresponding
+ * so->arrayKeys[] entries), and sets the corresponding attributes positions
+ * in *skipatts argument.  Every attribute that receives a skip array will
+ * have its skip support routine set in *skipatts, too.
+ */
+static int
+_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+							  int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_inkey = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				(*numSkipArrayKeys)++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				break;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatt
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatt->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										 BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack even an equality strategy
+	 * operator, but they're supported.  We cope by making caller not use a
+	 * skip array for this index column (nor for any lower-order columns).
+	 */
+	if (!OidIsValid(skipatt->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatt->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+													   reverse,
+													   &skipatt->sksup);
+
+	/* might not have set up skip support, but can skip either way */
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys_final() -- fix up array scan key references
  *
@@ -633,7 +983,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -669,6 +1020,14 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && !array->null_elem)
+						_bt_skip_preproc_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
@@ -684,7 +1043,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -980,26 +1339,28 @@ _bt_merge_arrays(IndexScanDesc scan, ScanKey skey, FmgrInfo *sortproc,
  * guaranteeing that at least the scalar scan key can be considered redundant.
  * We return false if the comparison could not be made (caller must keep both
  * scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar inequality scan keys.
  */
 static bool
 _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
 	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
 		   skey->sk_strategy != BTEqualStrategyNumber);
@@ -1009,8 +1370,7 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1041,11 +1401,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensures that the scalar scan key can be eliminated as redundant
+		 */
+		eliminated = true;
+
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+	}
+	else
+	{
+		/*
+		 * With a skip array, it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when there's a lack of suitable cross-type support for
+		 * comparing skey to an existing inequality associated with the array.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when
+ * determining its redundancy against a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1097,10 +1511,298 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of a skip array scan key when determining its
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array.  This forces the array's elements to be generated within the limits
+ * of a range that's described/constrained by the array's inequalities.
+ *
+ * Return value indicates if the scalar scan key could be "eliminated".  We
+ * return true in the common case where caller's scan key was successfully
+ * rolled into the skip array.  We return false when we can't do that due to
+ * the presence of an existing, conflicting inequality that cannot be compared
+ * to caller's inequality due to a lack of suitable cross-type support.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way scan code can generally assume that
+	 * it'll always be safe to use the ORDER procs stored in so->orderProcs[].
+	 * The only exception is cases where the array's scan key is NEGPOSINF.
+	 *
+	 * When the array's scan key is marked NEGPOSINF, then it'll lack a valid
+	 * sk_argument.  The scan must apply the array's low_compare and/or
+	 * high_compare (if any) to establish whether a given tupdatum is within
+	 * the range of the skip array.  This could involve applying the 3-way
+	 * low_order/high_order ORDER proc we store here (though never the ORDER
+	 * proc stored in so->orderProcs[]).
+	 *
+	 * Note: we sometimes use low_compare/high_compare inequalities with the
+	 * array's current element value, and with values that were mutated by the
+	 * array's skip support routine.  A skip array must always use its index
+	 * attribute's own input opclass/type, so this will always work correctly.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (!array->high_compare)
+			{
+				/* array currently lacks a high_compare, make space for one */
+				array->high_compare = palloc(sizeof(ScanKeyData));
+				array->high_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare, keep old one */
+
+				/* replace old high_compare with caller's new one */
+			}
+
+			/* caller's high_compare becomes skip array's high_compare */
+			*array->high_compare = *skey;
+			*array->high_order = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (!array->low_compare)
+			{
+				/* array currently lacks a low_compare, make space for one */
+				array->low_compare = palloc(sizeof(ScanKeyData));
+				array->low_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare, keep old one */
+
+				/* replace old low_compare with caller's new one */
+			}
+
+			/* caller's low_compare becomes skip array's low_compare */
+			*array->low_compare = *skey;
+			*array->low_order = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
 
+/*
+ * Perform final preprocessing for a skip array.  Called when the skip array's
+ * final low_compare and high_compare have been chosen.
+ *
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts the
+ * operator strategies).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value NEGPOSINF, using >= instead of using >
+ * (or using <= instead of < during backwards scans) makes it safe to include
+ * lower-order scan keys in the insertion scan key (there must be lower-order
+ * scan keys after the skip array).  We will avoid an extra _bt_first to find
+ * the first value in the index > sk_argument, at least when the first real
+ * matching value in the index happens to be an exact match for the newly
+ * transformed (newly incremented/decremented) sk_argument value.
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01' -- which would be wrong.
+ */
+static void
+_bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+	Assert(!array->null_elem);
+
+	/* If skip array doesn't have skip support, can't apply optimization */
+	if (!array->use_sksup)
+		return;
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skip_preproc_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skip_preproc_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skip_preproc_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+	Assert(array->use_sksup);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type.
+	 *
+	 * Note: we have to support the convention that sk_subtype == InvalidOid
+	 * means the opclass input type.
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup.decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skip_preproc_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+	Assert(array->use_sksup);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type.
+	 *
+	 * Note: we have to support the convention that sk_subtype == InvalidOid
+	 * means the opclass input type.
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup.increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  * qsort_arg comparator for sorting array elements
  */
@@ -1220,6 +1922,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1342,6 +2046,98 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, because the array doesn't really
+ * have any elements (it generates its array elements procedurally instead).
+ * Note that this may include a NULL value/an IS NULL qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ *
+ * Note: This function doesn't actually binary search.  It uses an interface
+ * that's as similar to _bt_binsrch_array_skey as possible for consistency.
+ * "Binary searching a skip array" just means determining if tupdatum/tupnull
+ * are before, within, or beyond the range of caller's skip array.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1351,29 +2147,480 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting skip array to lowest element, which isn't just NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Setting skip array to highest element, which isn't just NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element is out of bounds for the array.
+		 *
+		 * This is only possible if low_compare represents an >= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the prior value cannot possibly satisfy low_compare, so we can
+		 * give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(array->low_order,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Decrement by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &uflow);
+	if (uflow)
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the bounds of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element is out of bounds for the array.
+		 *
+		 * This is only possible if high_compare represents an <= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the next value cannot possibly satisfy high_compare, so we can
+		 * give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(array->high_order,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Increment by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &oflow);
+	if (oflow)
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the bounds of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1389,6 +2636,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1398,29 +2646,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1480,6 +2729,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1487,7 +2737,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1499,16 +2748,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1633,9 +2876,93 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (likely(!(cur->sk_flags & SK_BT_NEGPOSINF)))
+		{
+			/* Scankey has a valid/comparable sk_argument value (not -inf) */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXT))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument + infinitesimal"
+				 */
+				if (ScanDirectionIsForward(dir))
+				{
+					/* tupdatum is still < "sk_argument + infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was forward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to backward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum as its current element.
+				 */
+				return false;
+			}
+			else if (result == 0 && (cur->sk_flags & SK_BT_PRIOR))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument - infinitesimal"
+				 */
+				if (ScanDirectionIsBackward(dir))
+				{
+					/* tupdatum is still > "sk_argument - infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was backward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to forward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum as its current element.
+				 */
+				return false;
+			}
+		}
+		else
+		{
+			/*
+			 * Scankey searches for the sentinel value -inf (or +inf).
+			 *
+			 * Note: -inf could mean "true negative infinity", or it could
+			 * just represent the lowest/first value that satisfies the skip
+			 * array's low_compare.  +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * "Compare tupdatum against -inf" using array's low_compare, if
+			 * any (or "compare it against +inf" using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is >= -inf and <= +inf.  It's time for caller to
+				 * advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1967,18 +3294,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -2003,18 +3321,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2032,12 +3341,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -2113,11 +3431,62 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  "Binary searching" only determined whether
+		 * tupdatum is beyond, before, or within the range of the skip array.
+		 *
+		 * As always, we set the array element to its closest available match.
+		 * But unlike with a conventional array, a skip array's new element
+		 * might be NEGPOSINF, which represents the lowest (or the highest)
+		 * real value that is within the range of the skip array.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == "some array element".
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2467,6 +3836,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2479,10 +3850,24 @@ end_toplevel_scan:
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never consed up skip array scan keys, it would be possible for "gaps"
+ * to appear that made it unsafe to mark any subsequent input scan keys (taken
+ * from scan->keyData[]) as required to continue the scan.  If there are no
+ * keys for a given attribute, the keys for subsequent attributes can never be
+ * required.  For instance, "WHERE y = 4" always required a full-index scan
+ * prior to Postgres 18.  Typically, preprocessing is now able to "rewrite"
+ * such a qual into "WHERE x = ANY('{every possible x value}') and y = 4".
+ * The addition of a consed up skip array key on "x" enables marking the key
+ * on "y" (as well as the one on "x") required, according to the usual rules.
+ * Of course, this transformation process cannot change the tuples returned by
+ * the scan -- the skip array will use an IS NULL qual when searching for its
+ * "NULL element" (in this particular example, at least).  But skip arrays can
+ * make the scan much more efficient: if there are few distinct values in "x",
+ * we'll be able to skip over many irrelevant leaf pages.  (If on the other
+ * hand there are many distinct values in "x" then the scan will degenerate
+ * into a full index scan at run time.)
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -2519,7 +3904,12 @@ end_toplevel_scan:
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -2583,6 +3973,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -2671,7 +4069,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -2728,7 +4127,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -2776,6 +4174,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * Typically, our call to _bt_preprocess_array_keys will have
+			 * already added "=" skip array keys as required to form an
+			 * unbroken series of "=" constraints on all attrs prior to the
+			 * attr from the last scan key that came from our scan->keyData[]
+			 * input scan keys.  However, certain implementation restrictions
+			 * might have prevented array preprocessing from doing that for us
+			 * (e.g., opclasses without an "=" operator can't use skip scan).
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -2864,6 +4270,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -2872,6 +4279,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -2891,8 +4299,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -3034,10 +4440,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3103,6 +4510,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -3112,6 +4522,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3185,6 +4611,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3760,6 +5187,21 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  tupdesc,
 							  &isNull);
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
+
 		if (key->sk_flags & SK_ISNULL)
 		{
 			/* Handle IS NULL/NOT NULL tests */
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index b8b5c147c..a86dbf71b 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1330,6 +1330,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index db6ed784a..86a5de8f4 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 16144c2b7..2cef9816e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -370,6 +370,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 85e5eaf32..88c929a0a 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -98,6 +98,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 8130f3e8a..256350007 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -455,6 +456,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f73f294b8..cb8470324 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -85,6 +85,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 08fa6774d..42fde7ef2 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -192,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -213,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5736,6 +5740,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6794,6 +6884,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6803,17 +6940,22 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6829,14 +6971,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -6846,13 +6993,90 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't absord a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6874,6 +7098,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6894,7 +7119,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6910,6 +7135,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6925,6 +7182,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -7033,104 +7291,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index e00cd3c96..213a6326f 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2286,6 +2287,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 5284d23dc..654bda638 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -384,6 +387,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 8a67f0120..6befee3a4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1751,6 +1752,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3590,6 +3602,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 92b13f539..6fa688769 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -206,6 +206,7 @@ CREATE INDEX
                Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..12.04 rows=500 width=0) (never executed)
                Index Cond: (i2 = 898732)
+               Index Searches: 0
  Planning Time: 0.491 ms
  Execution Time: 0.055 ms
 (10 rows)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index b003492c5..0ee4ece8c 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2230,7 +2230,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2250,7 +2250,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index ebf2e3f85..dcefb4868 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4487,24 +4487,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -7609,19 +7610,23 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                        QUERY PLAN                         
------------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Nested Loop
    ->  Hash Join
          Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-         ->  Seq Scan on fkest f2
-               Filter: (x100 = 2)
+         ->  Bitmap Heap Scan on fkest f2
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
          ->  Hash
-               ->  Seq Scan on fkest f1
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f1
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
          Index Cond: (x = f1.x)
-(10 rows)
+(14 rows)
 
 alter table fkest add constraint fk
   foreign key (x, x10b, x100) references fkest (x, x10, x100);
@@ -7630,20 +7635,24 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                     QUERY PLAN                      
------------------------------------------------------
+                            QUERY PLAN                             
+-------------------------------------------------------------------
  Hash Join
    Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
    ->  Hash Join
          Hash Cond: (f3.x = f2.x)
          ->  Seq Scan on fkest f3
          ->  Hash
-               ->  Seq Scan on fkest f2
-                     Filter: (x100 = 2)
+               ->  Bitmap Heap Scan on fkest f2
+                     Recheck Cond: (x100 = 2)
+                     ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                           Index Cond: (x100 = 2)
    ->  Hash
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-(11 rows)
+         ->  Bitmap Heap Scan on fkest f1
+               Recheck Cond: (x100 = 2)
+               ->  Bitmap Index Scan on fkest_x_x10_x100_idx
+                     Index Cond: (x100 = 2)
+(15 rows)
 
 rollback;
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 36dc31c16..aef239200 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5240,9 +5240,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index c73631a9a..62eb4239a 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1461,18 +1461,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index 216bd9660..26721e7e0 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -852,7 +852,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -862,7 +862,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b54428b38..f182a76ef 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2667,6 +2668,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

#55Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Peter Geoghegan (#54)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 2024-11-26 07:32, Peter Geoghegan wrote:

On Mon, Nov 25, 2024 at 4:07 AM Masahiro Ikeda
<ikedamsh@oss.nttdata.com> wrote:

One thing I am concerned about is that it reduces the cases where the
optimization of _bt_checkkeys_look_ahead() works effectively, as the
approach
skips the skip immediately on the first occurrence per page.

I noticed that with the recent v17 revision of the patch, my original
MDAM paper "sales_mdam_paper" test case (the complicated query in the
introductory email of this thread) was about 2x slower. That's just
not okay, obviously. But the issue was relatively easy to fix: it was
fixed by making _bt_readpage not apply the "skipskip" optimization
when on the first page for the current primitive index scan -- we
already do this with the "precheck" optimization, so it's natural to
do it with the "skipskip" optimization as well.

The "sales_mdam_paper" test case involves thousands of primitive index
scans that each access only one leaf page. But each leaf page returns
2 non-adjoining tuples, with quite a few non-matching tuples "in
between" the matching tuples. There is one matching tuple for "store =
200", and another for "store = 250" -- and there's non-matching stores
201 - 249 between these two, which we want _bt_checkkeys_look_ahead to
skip over. This is exactly the kind of case where the
_bt_checkkeys_look_ahead() optimization is expected to help.

Great! Your new approach strikes a good balance between the trade-offs
of "skipskip" and "look ahead" optimization. Although the regression
case
I provided seems to be a corner case, your regression case is realistic
and should be addressed.

Again, the above results are provided for reference, as I believe that
many users prioritize stability and I'd like to take your new
approach.

Adversarial cases specifically designed to "make the patch look bad"
are definitely useful review feedback. Ideally, the patch will be 100%
free of regressions -- no matter how unlikely (or even silly) they may
seem. I always prefer to not have to rely on anybody's opinion of what
is likely or unlikely. :-)

A quick test seems to show that this particular regression is more or
less fixed by v18. As you said, the _bt_checkkeys_look_ahead stuff is
the issue here (same with the MDAM sales query). You should confirm
that the issue has actually been fixed, though.

Thanks to your new patch, I have confirmed that the issue is fixed.

I have no comments on the new patches. If I find any new regression
cases, I'll report them.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

In reply to: Masahiro Ikeda (#55)
3 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Thu, Nov 28, 2024 at 9:33 PM Masahiro Ikeda <ikedamsh@oss.nttdata.com> wrote:

Thanks to your new patch, I have confirmed that the issue is fixed.

I have no comments on the new patches. If I find any new regression
cases, I'll report them.

Attached is v19.

This is just to fix bitrot. The patch series stopped applying against
HEAD cleanly due to recent work that made EXPLAIN ANALYZE show buffers
output by default.

--
Peter Geoghegan

Attachments:

v19-0003-Add-skipskip-nbtree-skip-scan-optimization.patchapplication/x-patch; name=v19-0003-Add-skipskip-nbtree-skip-scan-optimization.patchDownload
From a2fc33978acfb6bd440685d855e50ee28466e3cf Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v19 3/3] Add "skipskip" nbtree skip scan optimization.

Fix regressions in cases that are nominally eligible to use skip scan
but can never actually benefit from skipping.  These are cases where the
leading skipped prefix column contains many distinct values -- often as
many distinct values are there are total index tuples.

For example, the test case posted here is fixed by the work from this
commit:

https://postgr.es/m/51d00219180323d121572e1f83ccde2a@oss.nttdata.com

Note that this commit doesn't actually change anything about when or how
skip scan decides when or how to skip.  It just avoids wasting CPU
cycles on uselessly maintaining a skip array at the tuple granularity,
preferring to maintain the skip arrays at something closer to the page
granularity when that makes sense.  See:

https://www.postgresql.org/message-id/flat/CAH2-Wz%3DE7XrkvscBN0U6V81NK3Q-dQOmivvbEsjG-zwEfDdFpg%40mail.gmail.com#7d34e8aa875d7a718043834c5ef4c167

Doing well on cases like this is important because we can't expect the
optimizer to never choose an affected plan -- we prefer to solve these
problems in the executor, which has access to the most reliable and
current information about the index.  The optimizer can afford to be
very optimistic about skipping if actual runtime scan behavior is very
similar to a traditional full index scan in the worst case.  See
"optimizer" section from the original intro mail for more information:

https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP%2BG4bw%40mail.gmail.com

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h           |   9 ++
 src/backend/access/nbtree/nbtree.c    |   1 +
 src/backend/access/nbtree/nbtsearch.c |  51 ++++++-
 src/backend/access/nbtree/nbtutils.c  | 206 +++++++++++++++++++++++---
 4 files changed, 238 insertions(+), 29 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index d841e85bc..e3b6f200d 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1051,6 +1051,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Last array advancement matched -inf attr? */
 	bool		oppositeDirCheck;	/* explicit scanBehind recheck needed? */
@@ -1111,6 +1112,14 @@ typedef struct BTReadPageState
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
 
+	/*
+	 * Input and output parameters, set and unset by both _bt_readpage and
+	 * _bt_checkkeys to manage "skipskip" optimization during skip scans
+	 */
+	bool		skipskip;
+	bool		noskipskip;
+	bool		advanced;
+
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
 	 * (only used during scans with array keys)
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 294eb1eb4..c96d7a446 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -423,6 +423,7 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 		memcpy(scan->keyData, scankey, scan->numberOfKeys * sizeof(ScanKeyData));
 	so->numberOfKeys = 0;		/* until _bt_preprocess_keys sets it */
 	so->numArrayKeys = 0;		/* ditto */
+	so->skipScan = false;		/* ditto */
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 43e321896..b88db6afe 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1644,6 +1644,27 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.continuescan = true; /* default assumption */
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
+
+	/*
+	 * Initialize "skipskip" optimization state (used only during scans with
+	 * skip arrays).
+	 *
+	 * Skip scans use this to manage the overhead of maintaining skip arrays
+	 * on columns with many distinct values.  It also works as a substitute
+	 * for the pstate.prechecked optimization, which skip scan never uses.
+	 *
+	 * We never do this for the first page read by each primitive scan.  This
+	 * avoids slowing down queries with skip arrays that have relatively few
+	 * distinct values -- the "look ahead" optimization is preferred there.
+	 */
+	pstate.skipskip = false;
+	pstate.noskipskip = firstPage;
+	pstate.advanced = false;
+
+	/*
+	 * Initialize "look ahead" optimization state (used only during scans with
+	 * arrays, including those that just use skip arrays)
+	 */
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
@@ -1660,10 +1681,10 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * corresponding value from the last item on the page.  So checking with
 	 * the last item on the page would give a more precise answer.
 	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
+	 * We don't do this for the first page read by each (primitive) scan, to
+	 * avoid slowing down point queries.  They typically don't stand to gain
+	 * much when the optimization can be applied, and are more likely to
+	 * notice the overhead of the precheck.  Also avoid it during skip scans.
 	 *
 	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
 	 * just set a low-order required array's key to the best available match
@@ -1687,7 +1708,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!firstPage && !so->scanBehind && minoff < maxoff)
+	if (!firstPage && !so->skipScan && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
@@ -1837,6 +1858,16 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
+			if (pstate.skipskip)
+			{
+				/*
+				 * reset array keys for finaltup call, since skipskip
+				 * optimization prevented ordinary array maintenance
+				 */
+				Assert(so->skipScan);
+				_bt_start_array_keys(scan, dir);
+				pstate.skipskip = false;
+			}
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -1897,6 +1928,16 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff && pstate.skipskip)
+			{
+				/*
+				 * reset array keys for finaltup call, since skipskip
+				 * optimization prevented ordinary array maintenance
+				 */
+				Assert(so->skipScan);
+				_bt_start_array_keys(scan, dir);
+				pstate.skipskip = false;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index a40635998..a1089e5e4 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -151,13 +151,16 @@ static bool _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption);
 static void _bt_mark_scankey_required(ScanKey skey);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool skipskip,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
 								 ScanDirection dir, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
-									 int tupnatts, TupleDesc tupdesc);
+									 IndexTuple tuple, int tupnatts, TupleDesc tupdesc);
+static bool _bt_checkkeys_skipskip(IndexScanDesc scan, BTReadPageState *pstate,
+								   IndexTuple tuple, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
 						   IndexTuple firstright, BTScanInsert itup_key);
 
@@ -354,6 +357,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	 */
 	numArrayKeys = _bt_preprocess_num_array_keys(scan, skipatts,
 												 &numSkipArrayKeys);
+	so->skipScan = (numSkipArrayKeys > 0);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
@@ -3205,8 +3209,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -3353,7 +3355,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -3395,7 +3397,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -3410,7 +3412,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -3522,7 +3524,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -3591,6 +3593,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		return false;
 	}
 
+	/* _bt_readpage must have unset skipskip flag (for finaltup call) */
+	Assert(!pstate->skipskip);
+
 	/*
 	 * Postcondition array state assertion (for still-unsatisfied tuples).
 	 *
@@ -3760,6 +3765,23 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		pstate->skip = pstate->maxoff + 1;
 	}
 
+	/*
+	 * Optimization: if a scan with a skip array doesn't satisfy every
+	 * required key (in practice this is almost always all the scan's keys),
+	 * we assume that this page isn't likely to skip "within" a page using
+	 * _bt_checkkeys_look_ahead.  We'll apply the 'skipskip' optimization.
+	 *
+	 * The 'skipskip' optimization allows _bt_checkkeys/_bt_check_compare to
+	 * stop maintaining the scan's skip arrays until we've reached finaltup.
+	 */
+	else if (so->skipScan && !pstate->noskipskip && pstate->advanced &&
+			 _bt_checkkeys_skipskip(scan, pstate, tuple, tupdesc))
+	{
+		pstate->skipskip = true;
+	}
+
+	pstate->advanced = true;	/* remember arrays advanced on page */
+
 	/* Caller's tuple doesn't match the new qual */
 	return false;
 
@@ -3861,7 +3883,8 @@ end_toplevel_scan:
  * make the scan much more efficient: if there are few distinct values in "x",
  * we'll be able to skip over many irrelevant leaf pages.  (If on the other
  * hand there are many distinct values in "x" then the scan will degenerate
- * into a full index scan at run time.)
+ * into a full index scan at run time, but we'll be no worse off overall.
+ * _bt_checkkeys's 'skipskip' optimization keeps the runtime overhead low.)
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -4903,7 +4926,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+							arrayKeys, pstate->skipskip,
+							pstate->prechecked, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
@@ -4915,7 +4939,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
 		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
+		Assert(!pstate->skipskip && !pstate->prechecked &&
+			   !pstate->firstmatch);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
@@ -4929,7 +4954,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, pstate->skipskip, false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -5002,7 +5027,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 			if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
 			{
 				/* See if we should skip ahead within the current leaf page */
-				_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
+				_bt_checkkeys_look_ahead(scan, pstate, tuple, tupnatts,
+										 tupdesc);
 
 				/*
 				 * Might have set pstate.skip to a later page offset.  When
@@ -5060,7 +5086,7 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	Assert(so->numArrayKeys);
 
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -5099,17 +5125,24 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Though we advance non-required array keys on our own, that shouldn't have
  * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
+ * have no fixed relationship with the scan's progress.  (skipskip=true makes
+ * us treat required scan keys as non-required, though.  That's safe because
+ * _bt_readpage will account for our failure to fully maintain the scan's
+ * arrays later on, right before its finaltup call to _bt_checkkeys.)
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass skipskip=true to instruct us to skip all of the scan's skip arrays.
+ * This provides the scan with a way of keeping the cost of maintaining its
+ * skip arrays under control, given skip arrays on high cardinality columns
+ * (i.e. given a skip array on a column which isn't a good fit for skip scan).
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool skipskip,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -5126,10 +5159,18 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when reading a page that's now using the "skipskip" optimization)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (skipskip)
+		{
+			Assert(!prechecked);
+
+			if (key->sk_flags & SK_BT_SKIP)
+				continue;
+		}
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
@@ -5241,7 +5282,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -5259,7 +5300,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -5511,13 +5552,19 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
  */
 static void
 _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
-						 int tupnatts, TupleDesc tupdesc)
+						 IndexTuple tuple, int tupnatts, TupleDesc tupdesc)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ScanDirection dir = so->currPos.dir;
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	/*
+	 * The "look ahead" skipping mechanism cannot be used at the same time as
+	 * skip scan's similar "skipskip" mechanism
+	 */
+	Assert(!pstate->skipskip);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
@@ -5563,6 +5610,9 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 			pstate->skip = aheadoffnum + 1;
 		else
 			pstate->skip = aheadoffnum - 1;
+
+		/* Don't attempt skipskip optimization on this page from here on */
+		pstate->noskipskip = true;
 	}
 	else
 	{
@@ -5575,9 +5625,117 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		pstate->rechecks = 0;
 		pstate->targetdistance = Max(pstate->targetdistance / 8, 1);
+
+		/*
+		 * During skip scan (when skip arrays are in use), pages containing
+		 * tuple where an omitted prefix column (a column corresponding to a
+		 * skip array) has many distinct values are a challenge.
+		 *
+		 * The 'skipskip' optimization allows _bt_checkkeys/_bt_check_compare
+		 * to stop maintaining the scan's skip arrays until we've reached
+		 * finaltup.
+		 */
+		if (so->skipScan && !pstate->noskipskip)
+		{
+			if (_bt_checkkeys_skipskip(scan, pstate, tuple, tupdesc))
+				pstate->skipskip = true;
+		}
 	}
 }
 
+/*
+ * Can _bt_checkkeys/_bt_check_compare apply the 'skipskip' optimization?
+ *
+ * Return value indicates if the optimization is safe for the tuples on the
+ * page after caller's tuple, but before its page's finaltup.
+ */
+static bool
+_bt_checkkeys_skipskip(IndexScanDesc scan, BTReadPageState *pstate,
+					   IndexTuple tuple, TupleDesc tupdesc)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	Relation	rel = scan->indexRelation;
+	ScanDirection dir = so->currPos.dir;
+	IndexTuple	finaltup = pstate->finaltup;
+	int			arrayidx = 0,
+				nfinaltupatts = 0;
+	bool		rangearrayseen = false;
+
+	Assert(!BTreeTupleIsPivot(tuple));
+	Assert(tuple != finaltup);
+
+	if (finaltup)
+		nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+	for (int ikey = 0; ikey < so->numberOfKeys; ikey++)
+	{
+		ScanKey		cur = so->keyData + ikey;
+		BTArrayKeyInfo *array = NULL;
+		Datum		tupdatum;
+		bool		tupnull;
+		int32		result;
+
+		/*
+		 * Only need to check range skip arrays within this loop.
+		 *
+		 * A SAOP array can always be treated as a non-required array within
+		 * _bt_check_compare.  A skip array without a lower or upper bound is
+		 * always safe to skip within _bt_check_compare, since it is satisfied
+		 * by every possible value.
+		 */
+		if (cur->sk_strategy != BTEqualStrategyNumber)
+			continue;
+		if (!(cur->sk_flags & SK_SEARCHARRAY))
+			continue;
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == ikey);
+		if (array->num_elems != -1 || array->null_elem)
+			continue;
+
+		/*
+		 * Found a range skip array to test.
+		 *
+		 * Scans with more than one range skip array are not eligible to use
+		 * the optimization.  Note that we support the skipskip optimization
+		 * for a qual like "WHERE a BETWEEN 1 AND 10 AND b BETWEEN 1 AND 3",
+		 * since there the qual actually requires only a single skip array.
+		 * However, if such a qual ended with "... AND C > 42", then it will
+		 * prevent use of the skipskip optimization.
+		 */
+		if (rangearrayseen)
+			return false;
+
+		/*
+		 * Don't attempt the optimization when we have a skip array and are
+		 * reading the rightmost leaf page (or the leftmost leaf page, when
+		 * scanning backwards)
+		 */
+		if (!finaltup)
+			return false;
+		rangearrayseen = true;
+
+		/* test the tuple that just advanced arrays within our caller */
+		Assert(cur->sk_flags & SK_BT_SKIP);
+		Assert(cur->sk_flags & SK_BT_REQFWD);
+		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], false, dir,
+								   tupdatum, tupnull, array, cur, &result);
+		if (result != 0)
+			return false;
+
+		/* test the page's finaltup iff relevant attribute isn't truncated */
+		if (cur->sk_attno > nfinaltupatts)
+			continue;
+
+		tupdatum = index_getattr(finaltup, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], false, dir,
+								   tupdatum, tupnull, array, cur, &result);
+		if (result != 0)
+			return false;
+	}
+
+	return true;
+}
+
 /*
  * _bt_killitems - set LP_DEAD state for items an indexscan caller has
  * told us were killed
-- 
2.45.2

v19-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/x-patch; name=v19-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From 05b40acb6bac4c7b80746ee9f3a1f716a01f8803 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v19 1/3] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 contrib/bloom/blscan.c                        |   1 +
 doc/src/sgml/bloom.sgml                       |   7 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 22 files changed, 244 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index e1884acf4..7b4180db5 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -153,6 +153,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 3aedec882..2fd284fc0 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -585,6 +585,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index f2fd62afb..5e423e155 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index b35b8a975..36f1435cb 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index 0d99d6abc..927ba1039 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 4b4ebff6a..a7adf9709 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 77afa1489..2441eaaaa 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -69,6 +69,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -552,6 +553,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -578,6 +580,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -705,6 +708,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			Assert(btscan->btps_nextScanPage != P_NONE);
 			*next_scan_page = btscan->btps_nextScanPage;
@@ -805,6 +813,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -839,6 +849,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 0cd046613..82bb93d1a 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -970,6 +970,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 301786185..be668abf2 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a201ed308..33ea21f38 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2105,6 +2107,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2118,6 +2121,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2134,6 +2138,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2652,6 +2657,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index 0c5fb725e..f77f716f0 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -116,6 +116,7 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++)
 	{
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 6a8a60b8c..4cddfaae1 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -174,9 +174,10 @@ CREATE INDEX
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..178436.00 rows=1 width=0) (actual time=20.005..20.005 rows=2300 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
          Buffers: shared hit=19608
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 22.632 ms
-(10 rows)
+(11 rows)
 </programlisting>
   </para>
 
@@ -209,12 +210,14 @@ CREATE INDEX
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..4.52 rows=11 width=0) (actual time=0.026..0.026 rows=7 loops=1)
                Index Cond: (i5 = 123451)
                Buffers: shared hit=3
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..4.52 rows=11 width=0) (actual time=0.007..0.007 rows=8 loops=1)
                Index Cond: (i2 = 898732)
                Buffers: shared hit=3
+               Index Searches: 1
  Planning Time: 0.264 ms
  Execution Time: 0.047 ms
-(13 rows)
+(15 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 840d7f816..c68eb770e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4198,12 +4198,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aab..34225c010 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -730,9 +730,11 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
                Buffers: shared hit=2
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
          Buffers: shared hit=24 read=6
+         Index Searches: 1
  Planning:
    Buffers: shared hit=15 dirtied=9
  Planning Time: 0.485 ms
@@ -791,6 +793,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
                            Buffers: shared hit=2
+                           Index Searches: 1
  Planning:
    Buffers: shared hit=12
  Planning Time: 0.187 ms
@@ -860,6 +863,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
    Buffers: shared hit=1
  Planning Time: 0.039 ms
@@ -894,8 +898,10 @@ EXPLAIN (ANALYZE, BUFFERS OFF) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND un
    -&gt;  BitmapAnd  (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
  Planning Time: 0.162 ms
  Execution Time: 0.143 ms
 </screen>
@@ -924,6 +930,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
                Buffers: shared read=2
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1059,6 +1066,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
    Buffers: shared hit=16
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
          Buffers: shared hit=16
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index 6361a14e6..6fc9bfedc 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -506,9 +506,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
          Buffers: shared hit=4
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(9 rows)
+(10 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index f2d146581..a5df4107a 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 5ecf971da..d9df5c0e1 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index c52bc40e8..b053891b6 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2657,47 +2661,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2713,16 +2726,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2741,7 +2757,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2757,16 +2773,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2787,7 +2806,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2877,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2897,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,17 +2986,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2982,17 +3013,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3064,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3048,17 +3091,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3112,17 +3161,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3144,17 +3199,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3482,12 +3543,14 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3566,10 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3617,17 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4197,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 88911ca2b..cde2eac73 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index d5aab4e56..2197b0ac5 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index d67598d5c..5492bd6e9 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

v19-0002-Add-skip-scan-to-nbtree.patchapplication/x-patch; name=v19-0002-Add-skip-scan-to-nbtree.patchDownload
From 967d078d011b29aa582076459b85c15a3e8245b9 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v19 2/3] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
useful in cases where the total number of distinct values in the column
'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.

The design of skip arrays allows array advancement to work without
concern for which specific array types are involved; only the lowest
level code needs to treat skip arrays and SAOP arrays differently.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values are conceptually just like any
other value that might appear in either kind of array.  A call made to
_bt_first to reposition the scan based on a NEXT/PRIOR sentinel usually
won't locate a leaf page containing tuples that must be returned to the
scan, but that's normal during any skip scan that happens to involve
"sparse" indexed values (skip support can only help with "dense" indexed
values, which isn't meaningfully possible when the skipped column is of
a continuous type, where skip support typically can't be implemented).

The optimizer doesn't use distinct new index paths to represent index
skip scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  Skipping is possible during
eligible bitmap index scans, index scans, and index-only scans.  It's
also possible during eligible parallel scans.  An eligible scan is any
scan that nbtree preprocessing generates a skip array for, which happens
whenever doing so will enable later preprocessing to mark original input
scan keys (passed by the executor) as required to continue the scan.

There are hardly any limitations around where skip arrays/scan keys may
appear relative to conventional/input scan keys.  This is no less true
in the presence of conventional SAOP array scan keys, which may both
roll over and be rolled over by skip arrays.  For example, a skip array
on the column "b" is generated for "WHERE a = 42 AND c IN (5, 6, 7, 8)".
As always (since commit 5bf748b8), whether or not nbtree actually skips
depends in large part on physical index characteristics at runtime.
Preprocessing is optimistic about skipping working out: it applies
simple static rules to decide where to place skip arrays.  It's up to
array-related scan code to keep the overhead of maintaining the scan's
skip arrays under control when it turns out that skipping isn't helpful.

Preprocessing will never cons up a skip array for an index column that
has an equality strategy scan key on input, but will do so for indexed
columns that are only constrained by inequality strategy scan keys.
This allows the scan to skip using a range skip array.  These are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   28 +-
 src/include/catalog/pg_amproc.dat             |   22 +
 src/include/catalog/pg_proc.dat               |   27 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  101 +
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  273 +++
 src/backend/access/nbtree/nbtree.c            |  214 ++-
 src/backend/access/nbtree/nbtsearch.c         |   75 +-
 src/backend/access/nbtree/nbtutils.c          | 1690 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   46 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  383 +++-
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/timestamp.c             |   48 +
 src/backend/utils/adt/uuid.c                  |   71 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/btree.sgml                       |   34 +-
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   49 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |   10 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   30 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    5 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 35 files changed, 2954 insertions(+), 323 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index a4c0b43aa..7271ae2d2 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -206,7 +206,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 123fba624..d841e85bc 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1022,10 +1024,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* skip scan support (unless !use_sksup) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo   *low_order;		/* low_compare's ORDER proc */
+	FmgrInfo   *high_order;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1113,6 +1127,10 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on omitted prefix column */
+#define SK_BT_NEGPOSINF	0x00080000	/* -inf/+inf key (invalid sk_argument) */
+#define SK_BT_NEXT		0x00100000	/* positions scan > sk_argument */
+#define SK_BT_PRIOR		0x00200000	/* positions scan < sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1149,6 +1167,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1159,7 +1181,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5d7fe292b..c5af25806 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 957552400..725779787 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2260,6 +2275,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4447,6 +4465,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6290,6 +6311,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9332,6 +9356,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d70e6d37e..5b58739ad 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..d97e6059d
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,101 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining pairs of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code falls back on next-key sentinel values for any opclass that
+ * doesn't provide its own skip support function.  There is no benefit to
+ * providing skip support for an opclass on a type where guessing that the
+ * next indexed value is the next possible indexable value seldom works out.
+ * It might not even be feasible to add skip support for a continuous type.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order).
+	 * This gives the B-Tree code a useful value to start from, before any
+	 * data has been read from the index.  These are also sometimes used to
+	 * detect when the scan has run out of indexable values to search for.
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 *
+	 * Operator class decrement/increment functions never have to directly
+	 * deal with the value NULL, nor with ASC vs. DESC column ordering.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 1859be614..b4f1466d4 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 1c72867c8..26ebbf776 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 2441eaaaa..294eb1eb4 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -29,6 +29,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -70,7 +71,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -78,11 +79,24 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 *
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -535,10 +549,159 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume all input scankeys will be output with its own
+	 * SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * scan array (and associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		Form_pg_attribute attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array/skip scan key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		Form_pg_attribute attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array/skip scan key, while freeing memory allocated
+		 * for old sk_argument where required
+		 */
+		attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_NEGPOSINF)
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -549,7 +712,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -572,16 +736,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -608,6 +772,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -652,7 +817,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -674,14 +839,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -719,7 +879,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -763,11 +923,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -806,7 +966,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -815,7 +975,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -833,6 +993,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -842,7 +1003,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -852,14 +1013,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 82bb93d1a..43e321896 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -984,6 +984,13 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  Skip
+	 * array keys are = keys, though we'll sometimes need to treat the key as
+	 * if it was some kind of inequality instead.  This is required whenever
+	 * the skip array's current array element is a sentinel value.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1059,8 +1066,39 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is -inf or +inf, choose low_compare/high_compare
+				 * (or consider if they imply a NOT NULL constraint).
+				 */
+				if (chosen && (chosen->sk_flags & SK_BT_NEGPOSINF))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skiparraysk = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					if (!array->null_elem)
+						impliesNN = skiparraysk;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1097,6 +1135,39 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					strat_total == BTLessStrategyNumber)
 					break;
 
+				/*
+				 * If this is a scan key for a skip array whose current
+				 * element is marked NEXT or PRIOR, adjust strat_total
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(strat_total == BTEqualStrategyNumber);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[].
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).
+					 */
+					break;
+				}
+
 				/*
 				 * Done if that was the last attribute, or if next key is not
 				 * in sequence (implying no boundary key is available for the
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 50cbf06cb..a40635998 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	Oid			eq_op;			/* = op to be used to add array, if any */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -63,16 +91,46 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
+static int	_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+										  int *numSkipArrayKeys);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_decrement(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_increment(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -256,6 +314,12 @@ _bt_freestack(BTStack stack)
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -271,10 +335,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -282,30 +348,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_preprocess_num_array_keys(scan, skipatts,
+												 &numSkipArrayKeys);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys described within skipatts[]
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -320,17 +380,18 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/* Process input keys, and emit skip array keys described by skipatts[] */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -346,19 +407,97 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and its scan key where indicated */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* attribute already had an "=" key on input */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[numArrayKeyData];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].low_order = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].high_order = NULL;	/* for now */
+
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how this
+			 * constructed "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			numArrayKeyData++;	/* keep this scan key/array */
+		}
+
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
+		cur = &arrayKeyData[numArrayKeyData];
 		*cur = scan->keyData[input_ikey];
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -414,7 +553,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -425,7 +564,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -442,7 +581,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -517,23 +656,234 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].low_order = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].high_order = NULL;	/* unused */
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
 	return arrayKeyData;
 }
 
+/*
+ *	_bt_preprocess_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit all required so->arrayKeys[] entries.
+ *
+ * Also sets *numSkipArrayKeys to the number of skip arrays that caller must
+ * make sure to add to the scan keys it'll output (complete with corresponding
+ * so->arrayKeys[] entries), and sets the corresponding attributes positions
+ * in *skipatts argument.  Every attribute that receives a skip array will
+ * have its skip support routine set in *skipatts, too.
+ */
+static int
+_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+							  int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_inkey = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make best use of both of its precheck optimizations, but
+		 * _bt_first will be no less capable of efficiently finding the
+		 * starting position for each primitive index scan.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when preprocessing proper deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				(*numSkipArrayKeys)++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				break;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatt
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatt->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										 BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack even an equality strategy
+	 * operator, but they're supported.  We cope by making caller not use a
+	 * skip array for this index column (nor for any lower-order columns).
+	 */
+	if (!OidIsValid(skipatt->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatt->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+													   reverse,
+													   &skipatt->sksup);
+
+	/* might not have set up skip support, but can skip either way */
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys_final() -- fix up array scan key references
  *
@@ -633,7 +983,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -669,6 +1020,14 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && !array->null_elem)
+						_bt_skip_preproc_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
@@ -684,7 +1043,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -980,26 +1339,28 @@ _bt_merge_arrays(IndexScanDesc scan, ScanKey skey, FmgrInfo *sortproc,
  * guaranteeing that at least the scalar scan key can be considered redundant.
  * We return false if the comparison could not be made (caller must keep both
  * scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar inequality scan keys.
  */
 static bool
 _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
 	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
 		   skey->sk_strategy != BTEqualStrategyNumber);
@@ -1009,8 +1370,7 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1041,11 +1401,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensures that the scalar scan key can be eliminated as redundant
+		 */
+		eliminated = true;
+
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+	}
+	else
+	{
+		/*
+		 * With a skip array, it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when there's a lack of suitable cross-type support for
+		 * comparing skey to an existing inequality associated with the array.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when
+ * determining its redundancy against a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1097,10 +1511,292 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of a skip array scan key when determining its
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array.  This forces the array's elements to be generated within the limits
+ * of a range that's described/constrained by the array's inequalities.
+ *
+ * Return value indicates if the scalar scan key could be "eliminated".  We
+ * return true in the common case where caller's scan key was successfully
+ * rolled into the skip array.  We return false when we can't do that due to
+ * the presence of an existing, conflicting inequality that cannot be compared
+ * to caller's inequality due to a lack of suitable cross-type support.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way scan code can generally assume that
+	 * it'll always be safe to use the ORDER procs stored in so->orderProcs[].
+	 * The only exception is cases where the array's scan key is NEGPOSINF.
+	 *
+	 * When the array's scan key is marked NEGPOSINF, then it'll lack a valid
+	 * sk_argument.  The scan must apply the array's low_compare and/or
+	 * high_compare (if any) to establish whether a given tupdatum is within
+	 * the range of the skip array.  This could involve applying the 3-way
+	 * low_order/high_order ORDER proc we store here (though never the ORDER
+	 * proc stored in so->orderProcs[]).
+	 *
+	 * Note: we sometimes use low_compare/high_compare inequalities with the
+	 * array's current element value, and with values that were mutated by the
+	 * array's skip support routine.  A skip array must always use its index
+	 * attribute's own input opclass/type, so this will always work correctly.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (!array->high_compare)
+			{
+				/* array currently lacks a high_compare, make space for one */
+				array->high_compare = palloc(sizeof(ScanKeyData));
+				array->high_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare, keep old one */
+
+				/* replace old high_compare with caller's new one */
+			}
+
+			/* caller's high_compare becomes skip array's high_compare */
+			*array->high_compare = *skey;
+			*array->high_order = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (!array->low_compare)
+			{
+				/* array currently lacks a low_compare, make space for one */
+				array->low_compare = palloc(sizeof(ScanKeyData));
+				array->low_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare, keep old one */
+
+				/* replace old low_compare with caller's new one */
+			}
+
+			/* caller's low_compare becomes skip array's low_compare */
+			*array->low_compare = *skey;
+			*array->low_order = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
 
+/*
+ * Perform final preprocessing for a skip array.  Called when the skip array's
+ * final low_compare and high_compare have been chosen.
+ *
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts the
+ * operator strategies).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value NEGPOSINF, using >= instead of using >
+ * (or using <= instead of < during backwards scans) makes it safe to include
+ * lower-order scan keys in the insertion scan key (there must be lower-order
+ * scan keys after the skip array).  We will avoid an extra _bt_first to find
+ * the first value in the index > sk_argument, at least when the first real
+ * matching value in the index happens to be an exact match for the newly
+ * transformed (newly incremented/decremented) sk_argument value.
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01' -- which would be wrong.
+ */
+static void
+_bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+	Assert(!array->null_elem);
+
+	/* If skip array doesn't have skip support, can't apply optimization */
+	if (!array->use_sksup)
+		return;
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skip_preproc_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skip_preproc_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skip_preproc_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+	Assert(array->use_sksup);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup.decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skip_preproc_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+	Assert(array->use_sksup);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup.increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  * qsort_arg comparator for sorting array elements
  */
@@ -1220,6 +1916,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1342,6 +2040,98 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, because the array doesn't really
+ * have any elements (it generates its array elements procedurally instead).
+ * Note that this may include a NULL value/an IS NULL qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ *
+ * Note: This function doesn't actually binary search.  It uses an interface
+ * that's as similar to _bt_binsrch_array_skey as possible for consistency.
+ * "Binary searching a skip array" just means determining if tupdatum/tupnull
+ * are before, within, or beyond the range of caller's skip array.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1351,29 +2141,480 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting skip array to lowest element, which isn't just NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+	else
+	{
+		/* Setting skip array to highest element, which isn't just NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_NEGPOSINF;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value -inf is never decrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element is out of bounds for the array.
+		 *
+		 * This is only possible if low_compare represents an >= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the prior value cannot possibly satisfy low_compare, so we can
+		 * give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(array->low_order,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Decrement by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &uflow);
+	if (uflow)
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+	Form_pg_attribute attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_NEGPOSINF));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/* The sentinel value +inf is never incrementable */
+	if (skey->sk_flags & SK_BT_NEGPOSINF)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element is out of bounds for the array.
+		 *
+		 * This is only possible if high_compare represents an <= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the next value cannot possibly satisfy high_compare, so we can
+		 * give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(array->high_order,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Increment by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &oflow);
+	if (oflow)
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1389,6 +2630,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1398,29 +2640,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1480,6 +2723,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1487,7 +2731,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1499,16 +2742,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1633,9 +2870,93 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_NEGPOSINF))
+		{
+			/* Scankey has a valid/comparable sk_argument value (not -inf) */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXT))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument + infinitesimal"
+				 */
+				if (ScanDirectionIsForward(dir))
+				{
+					/* tupdatum is still < "sk_argument + infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was forward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to backward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum as its current element.
+				 */
+				return false;
+			}
+			else if (result == 0 && (cur->sk_flags & SK_BT_PRIOR))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument - infinitesimal"
+				 */
+				if (ScanDirectionIsBackward(dir))
+				{
+					/* tupdatum is still > "sk_argument - infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was backward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to forward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum as its current element.
+				 */
+				return false;
+			}
+		}
+		else
+		{
+			/*
+			 * Scankey searches for the sentinel value -inf (or +inf).
+			 *
+			 * Note: -inf could mean "true negative infinity", or it could
+			 * just represent the lowest/first value that satisfies the skip
+			 * array's low_compare.  +inf and high_compare work similarly.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * "Compare tupdatum against -inf" using array's low_compare, if
+			 * any (or "compare it against +inf" using array's high_compare).
+			 *
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum is >= -inf and <= +inf.  It's time for caller to
+				 * advance the scan's array keys.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1967,18 +3288,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -2003,18 +3315,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2032,12 +3335,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -2113,11 +3425,62 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  "Binary searching" only determined whether
+		 * tupdatum is beyond, before, or within the range of the skip array.
+		 *
+		 * As always, we set the array element to its closest available match.
+		 * But unlike with a conventional array, a skip array's new element
+		 * might be NEGPOSINF, which represents the lowest (or the highest)
+		 * real value that is within the range of the skip array.
+		 */
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == "some array element".
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2467,6 +3830,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2479,10 +3844,24 @@ end_toplevel_scan:
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never consed up skip array scan keys, it would be possible for "gaps"
+ * to appear that made it unsafe to mark any subsequent input scan keys (taken
+ * from scan->keyData[]) as required to continue the scan.  If there are no
+ * keys for a given attribute, the keys for subsequent attributes can never be
+ * required.  For instance, "WHERE y = 4" always required a full-index scan
+ * prior to Postgres 18.  Typically, preprocessing is now able to "rewrite"
+ * such a qual into "WHERE x = ANY('{every possible x value}') and y = 4".
+ * The addition of a consed up skip array key on "x" enables marking the key
+ * on "y" (as well as the one on "x") required, according to the usual rules.
+ * Of course, this transformation process cannot change the tuples returned by
+ * the scan -- the skip array will use an IS NULL qual when searching for its
+ * "NULL element" (in this particular example, at least).  But skip arrays can
+ * make the scan much more efficient: if there are few distinct values in "x",
+ * we'll be able to skip over many irrelevant leaf pages.  (If on the other
+ * hand there are many distinct values in "x" then the scan will degenerate
+ * into a full index scan at run time.)
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -2519,7 +3898,12 @@ end_toplevel_scan:
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -2583,6 +3967,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProc[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -2671,7 +4063,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -2728,7 +4121,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -2776,6 +4168,15 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, our call to _bt_preprocess_array_keys will have
+			 * already added "=" skip array keys as required to form an
+			 * unbroken series of "=" constraints on all attrs prior to the
+			 * attr from the last scan key that came from our scan->keyData[]
+			 * input scan keys.  However, certain implementation restrictions
+			 * might have prevented array preprocessing from doing that for us
+			 * (e.g., opclasses without an "=" operator can't use skip scan).
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -2864,6 +4265,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -2872,6 +4274,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -2891,8 +4294,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -3034,10 +4435,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3103,6 +4505,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -3112,6 +4517,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3185,6 +4606,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3746,6 +5168,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key might be negative/positive infinity.  Might
+		 * also be next key/prior key sentinel, which we don't deal with.
+		 */
+		if (key->sk_flags & (SK_BT_NEGPOSINF | SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60d..96d0d9185 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index d024c547c..431212535 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 9cf3e4f4f..4759624ef 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 16144c2b7..2cef9816e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -370,6 +370,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index da61ac0e8..0a108da39 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index e86d6dc8e..98da47963 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 08fa6774d..e7753b7c1 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -192,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -213,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5736,6 +5740,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6794,6 +6884,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6803,17 +6940,22 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6829,14 +6971,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -6846,13 +6993,90 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't absord a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6874,6 +7098,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6894,7 +7119,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6910,6 +7135,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6925,6 +7182,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -6953,7 +7211,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * natural ceiling on the worst case number of descents -- there
 		 * cannot possibly be more than one descent per leaf page scanned.
 		 *
-		 * Clamp the number of descents to at most 1/3 the number of index
+		 * Clamp the number of descents to at most the total number of index
 		 * pages.  This avoids implausibly high estimates with low selectivity
 		 * paths, where scans usually require only one or two descents.  This
 		 * is most likely to help when there are several SAOP clauses, where
@@ -6966,7 +7224,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * give the btree code credit for its ability to continue on the leaf
 		 * level with low selectivity scans.
 		 */
-		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
+		num_sa_scans = Min(num_sa_scans, index->pages);
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
@@ -7033,104 +7291,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..d91471e26
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 18d7d8a10..81e709a5a 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2286,6 +2287,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 5284d23dc..654bda638 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,12 +13,15 @@
 
 #include "postgres.h"
 
+#include <limits.h>
+
 #include "common/hashfn.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -384,6 +387,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 8cf1afbad..58c74cc59 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1761,6 +1762,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3609,6 +3621,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 1904eb65b..22380abae 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2248,7 +2248,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2268,7 +2268,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 51aeb1dae..167cab58c 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4550,24 +4550,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -8093,9 +8094,10 @@ where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1 and j2.id1 >= any (array[1,5]);
    Merge Cond: (j1.id1 = j2.id1)
    Join Filter: (j2.id2 = j1.id2)
    ->  Index Scan using j1_id1_idx on j1
-   ->  Index Scan using j2_id1_idx on j2
+   ->  Index Only Scan using j2_pkey on j2
          Index Cond: (id1 >= ANY ('{1,5}'::integer[]))
-(6 rows)
+         Filter: ((id1 % 1000) = 1)
+(7 rows)
 
 select * from j1
 inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 36dc31c16..aef239200 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5240,9 +5240,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index c73631a9a..62eb4239a 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1461,18 +1461,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index c085e05f0..6e3b9f123 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -858,7 +858,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -868,7 +868,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ce33e55bf..03d163045 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2668,6 +2669,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

In reply to: Peter Geoghegan (#56)
3 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Dec 11, 2024 at 2:13 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v19.

I now attach v20. This revision simplifies the "skipskip"
optimization, from the v20-0003-* patch. We now apply it on every page
that isn't the primitive index scan's first leaf page read (during
skip scans) -- we'll no longer activate it midway through scanning a
leaf page within _bt_readpage.

The newly revised "skipskip" optimization seems to get the regressions
down to only a 5% - 10% increase in runtime across a wide variety of
unsympathetic cases -- I'm now validating performance against a test
suite based on the adversarial cases presented by Masahiro Ikeda on
this thread. Although I think that I'll end up tuning the "skipskip"
mechanism some more (I may have been too conservative in marginal
cases that actually do benefit from skipping), I deem these
regressions to be acceptable. They're only seen in the most
unsympathetic cases, where an omitted leading column has groupings of
no more than about 50 index tuples, making skipping pretty hopeless.

I knew from the outset that the hardest part of this project would be
avoiding regressions in highly unsympathetic cases. The regressions
that are still there seem very difficult to minimize any further; the
overhead that remains comes from the simple need to maintain the
scan's skip arrays once per page, before leaving the page. Once a scan
decides to apply the "skipskip" optimization, it tends to stick with
it for all future leaf pages -- leaving only the overhead of checking
the high key while advancing the scan's arrays. I've cut just about
all that that I can reasonably cut from the hot code paths that are at
issue with the regressed cases.

It's important to have a sense of the context that these regressions
are seen in. We can reasonably hope that the optimizer wouldn't pick a
plan like this in the first place, and/or hope that the user would
create an appropriate index to avoid an inherently inefficient full
index scan (a scan like the one that I've regressed). Plus the
overhead only gets this high for index-only scans, where index
traversal costs will naturally dominate. If a user's query really is
made slower to the same degree (5% - 10%), then the user probably
doesn't consider the query very performance critical. They're unlikely
to notice the 5% - 10% regression -- creating the right index for the
job will make the query multiple times faster, at a minimum.

The break-even point where we should prefer to skip is pretty close to
a policy of simply always skipping -- especially with the "skipskip"
optimization/patch in place. That makes it seem unlikely that we could
do much better by giving the optimizer a greater role in things. I
just don't think that the optimizer has sufficiently accurate
information about the characteristics of the index to get anything
close to the level of precision that is required to avoid regressions.
For example, I see many queries that are ~5x faster than an equivalent
full index scan, despite only ever skipping over every second leaf
page -- there are still big savings in CPU costs for such cases. We
see big speedups in these "marginal" cases -- speedups that are really
hard to model using the available statistics. If a reliable cost
function could be built, then it would be very sensitive to its
parameters, and would exhibit very nonlinear behavior. In general,
something that behaves like that seems unlikely to ever be truly
reliable.

--
Peter Geoghegan

Attachments:

v20-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v20-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From 662a296711eccc6ac648eb9f52ca8eb24b9d1ff2 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v20 1/3] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 contrib/bloom/blscan.c                        |   1 +
 doc/src/sgml/bloom.sgml                       |   7 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 22 files changed, 244 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index dc6e01842..0d1ad8379 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -147,6 +147,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 9a9845475..0461022ef 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -585,6 +585,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index 7d1e66152..81440a283 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index cc40e928e..609e85fda 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index a3a1fccf3..c4f730437 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 07bae342e..369e39bc4 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 3d617f168..6345a37a8 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -69,6 +69,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -552,6 +553,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -578,6 +580,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -705,6 +708,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			Assert(btscan->btps_nextScanPage != P_NONE);
 			*next_scan_page = btscan->btps_nextScanPage;
@@ -805,6 +813,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -839,6 +849,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index ad7feccc1..9609bbda9 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -970,6 +970,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 986362a77..991f99b27 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c24e66f82..6b65037cd 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2105,6 +2107,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2118,6 +2121,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2134,6 +2138,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2652,6 +2657,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index bf801fe78..472169d61 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -116,6 +116,7 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++)
 	{
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 6a8a60b8c..4cddfaae1 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -174,9 +174,10 @@ CREATE INDEX
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..178436.00 rows=1 width=0) (actual time=20.005..20.005 rows=2300 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
          Buffers: shared hit=19608
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 22.632 ms
-(10 rows)
+(11 rows)
 </programlisting>
   </para>
 
@@ -209,12 +210,14 @@ CREATE INDEX
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..4.52 rows=11 width=0) (actual time=0.026..0.026 rows=7 loops=1)
                Index Cond: (i5 = 123451)
                Buffers: shared hit=3
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..4.52 rows=11 width=0) (actual time=0.007..0.007 rows=8 loops=1)
                Index Cond: (i2 = 898732)
                Buffers: shared hit=3
+               Index Searches: 1
  Planning Time: 0.264 ms
  Execution Time: 0.047 ms
-(13 rows)
+(15 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index d0d176cc5..23e88ca0b 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4198,12 +4198,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aab..34225c010 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -730,9 +730,11 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
                Buffers: shared hit=2
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
          Buffers: shared hit=24 read=6
+         Index Searches: 1
  Planning:
    Buffers: shared hit=15 dirtied=9
  Planning Time: 0.485 ms
@@ -791,6 +793,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
                            Buffers: shared hit=2
+                           Index Searches: 1
  Planning:
    Buffers: shared hit=12
  Planning Time: 0.187 ms
@@ -860,6 +863,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
    Buffers: shared hit=1
  Planning Time: 0.039 ms
@@ -894,8 +898,10 @@ EXPLAIN (ANALYZE, BUFFERS OFF) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND un
    -&gt;  BitmapAnd  (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
  Planning Time: 0.162 ms
  Execution Time: 0.143 ms
 </screen>
@@ -924,6 +930,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
                Buffers: shared read=2
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1059,6 +1066,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
    Buffers: shared hit=16
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
          Buffers: shared hit=16
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index 6361a14e6..6fc9bfedc 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -506,9 +506,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
          Buffers: shared hit=4
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(9 rows)
+(10 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index f2d146581..a5df4107a 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 5ecf971da..d9df5c0e1 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index c52bc40e8..b053891b6 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2657,47 +2661,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2713,16 +2726,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2741,7 +2757,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2757,16 +2773,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2787,7 +2806,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2877,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2897,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,17 +2986,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2982,17 +3013,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3064,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3048,17 +3091,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3112,17 +3161,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3144,17 +3199,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3482,12 +3543,14 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3566,10 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3617,17 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4197,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 88911ca2b..cde2eac73 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index d5aab4e56..2197b0ac5 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index d67598d5c..5492bd6e9 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.45.2

v20-0003-Lower-the-overhead-of-nbtree-runtime-skip-checks.patchapplication/octet-stream; name=v20-0003-Lower-the-overhead-of-nbtree-runtime-skip-checks.patchDownload
From a681977fc52ef25d58593ebee08d97d3f1ca84bb Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v20 3/3] Lower the overhead of nbtree runtime skip checks.

Add a new "skipskip" strategy to fix regressions in index scans that are
nominally eligible to use skip scan, but can never actually benefit from
skipping.  These are cases where a leading prefix column contains many
distinct values -- typically as many distinct values are there are total
index tuples.  This works by dynamically falling back on a strategy that
temporarily treats all user-supplied scan keys as if they were marked
non-required, while avoiding all skip array maintenance.  The new
optimization is applied for all pages beyond each primitive index scan's
first leaf page read.

Note that this commit doesn't actually change anything about when or how
skip scan decides to schedule new primitive index scans.  It is limited
to saving CPU cycles by varying how we read individual index tuples in
cases where maintaining skip arrays cannot possibly pay for itself.

Fixing (or significantly ameliorating) regressions in the worst case
like this enables skip scan's approach within the planner.  The planner
doesn't generate distinct index paths to represent nbtree index skip
scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  The planner considers skipping when
costing relevant index paths, but that in itself won't influence skip
scan's runtime behavior.

This approach makes skip scan adapt to skewed key distributions.  It
also makes planning less sensitive to misestimations.  An index path
with an inaccurate estimate of the total number of required primitive
index scans won't have to perform many more primitive scans at runtime
(it'll behave like a traditional full index scan instead).

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h           |   3 +
 src/backend/access/nbtree/nbtsearch.c |  55 ++++++++
 src/backend/access/nbtree/nbtutils.c  | 195 +++++++++++++++++++++-----
 3 files changed, 216 insertions(+), 37 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 9c53ba7e9..e4ee13dd0 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1111,6 +1111,7 @@ typedef struct BTReadPageState
 	 */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
+	bool		skipskip;		/* skip maintenance of skip arrays? */
 
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
@@ -1306,6 +1307,8 @@ extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arra
 						  IndexTuple tuple, int tupnatts);
 extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 								  IndexTuple finaltup);
+extern bool _bt_checkkeys_skipskip(IndexScanDesc scan, BTReadPageState *pstate,
+								   IndexTuple tuple, TupleDesc tupdesc);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index d332549cb..792146e7b 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1652,6 +1652,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.continuescan = true; /* default assumption */
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
+	pstate.skipskip = false;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
@@ -1744,6 +1745,23 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 
 				/* Deliberately don't unset scanBehind flag just yet */
 			}
+
+			/*
+			 * Skip maintenance of skip arrays (if any) during primitive index
+			 * scans that read leaf pages after the first
+			 */
+			if (so->skipScan && !firstPage)
+			{
+				IndexTuple	itup;
+
+				iid = PageGetItemId(page, offnum);
+				itup = (IndexTuple) PageGetItem(page, iid);
+				if (_bt_checkkeys_skipskip(scan, &pstate, itup,
+										   RelationGetDescr(rel)))
+				{
+					pstate.skipskip = true;
+				}
+			}
 		}
 
 		/* load items[] in ascending order */
@@ -1847,6 +1865,16 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
+			if (pstate.skipskip)
+			{
+				/*
+				 * reset array keys for finaltup call, since skipskip
+				 * optimization prevented ordinary array maintenance
+				 */
+				Assert(so->skipScan);
+				_bt_start_array_keys(scan, dir);
+				pstate.skipskip = false;
+			}
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -1866,6 +1894,23 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			ItemId		iid = PageGetItemId(page, minoff);
 
 			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+			/*
+			 * Skip maintenance of skip arrays (if any) during primitive index
+			 * scans that read leaf pages after the first
+			 */
+			if (so->skipScan && !firstPage)
+			{
+				IndexTuple	itup;
+
+				iid = PageGetItemId(page, offnum);
+				itup = (IndexTuple) PageGetItem(page, iid);
+				if (_bt_checkkeys_skipskip(scan, &pstate, itup,
+										   RelationGetDescr(rel)))
+				{
+					pstate.skipskip = true;
+				}
+			}
 		}
 
 		/* load items[] in descending order */
@@ -1907,6 +1952,16 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff && pstate.skipskip)
+			{
+				/*
+				 * reset array keys for finaltup call, since skipskip
+				 * optimization prevented ordinary array maintenance
+				 */
+				Assert(so->skipScan);
+				_bt_start_array_keys(scan, dir);
+				pstate.skipskip = false;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index ea8b34049..9d4f31dd0 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -151,11 +151,12 @@ static bool _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption);
 static void _bt_mark_scankey_required(ScanKey skey);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool skipskip,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-								 ScanDirection dir, bool *continuescan);
+								 ScanDirection dir, bool skipskip, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -3104,14 +3105,14 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
  * postcondition's <= operator with a >=.  In other words, just swap the
  * precondition with the postcondition.)
  *
- * We also deal with "advancing" non-required arrays here.  Callers whose
- * sktrig scan key is non-required specify sktrig_required=false.  These calls
- * are the only exception to the general rule about always advancing the
- * required array keys (the scan may not even have a required array).  These
- * callers should just pass a NULL pstate (since there is never any question
- * of stopping the scan).  No call to _bt_tuple_before_array_skeys is required
- * ahead of these calls (it's already clear that any required scan keys must
- * be satisfied by caller's tuple).
+ * We also deal with "advancing" non-required arrays here (or arrays that are
+ * temporarily being treated as non-required due to the application of the
+ * "skipskip" optimization).  Callers whose sktrig scan key is non-required
+ * specify sktrig_required=false.  These calls are the only exception to the
+ * general rule about always advancing the required array keys (the scan may
+ * not even have a required array).  These callers should just pass a NULL
+ * pstate (since there is never any question of stopping the scan).  No call
+ * to _bt_tuple_before_array_skeys is required ahead of these calls.
  *
  * Note that we deal with non-array required equality strategy scan keys as
  * degenerate single element arrays here.  Obviously, they can never really
@@ -3168,8 +3169,8 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		pstate->firstmatch = false;
 
-		/* Shouldn't have to invalidate 'prechecked', though */
-		Assert(!pstate->prechecked);
+		/* Shouldn't have to invalidate 'prechecked' or 'skipskip', though */
+		Assert(!pstate->prechecked && !pstate->skipskip);
 
 		/*
 		 * Once we return we'll have a new set of required array keys, so
@@ -3221,8 +3222,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -3369,7 +3368,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -3411,7 +3410,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -3426,7 +3425,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -3539,7 +3538,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -4909,7 +4908,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+							arrayKeys, pstate->skipskip,
+							pstate->prechecked, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
@@ -4921,7 +4921,8 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
 		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
+		Assert(!pstate->skipskip && !pstate->prechecked &&
+			   !pstate->firstmatch);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
@@ -4935,7 +4936,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, pstate->skipskip, false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -4958,6 +4959,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * It's also possible that the scan is still _before_ the _start_ of
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
+	Assert(!pstate->skipskip);
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
@@ -5066,7 +5068,7 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	Assert(so->numArrayKeys);
 
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -5105,17 +5107,24 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Though we advance non-required array keys on our own, that shouldn't have
  * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
+ * have no fixed relationship with the scan's progress.  (skipskip=true makes
+ * us treat required scan keys as non-required, though.  That's safe because
+ * _bt_readpage will account for our failure to fully maintain the scan's
+ * arrays later on, right before its finaltup call to _bt_checkkeys.)
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass skipskip=true to instruct us to skip all of the scan's skip arrays.
+ * This provides the scan with a way of keeping the cost of maintaining its
+ * skip arrays under control, given skip arrays on high cardinality columns
+ * (i.e. given a skip array on a column which isn't a good fit for skip scan).
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool skipskip,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -5132,10 +5141,18 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when reading a page that's now using the "skipskip" optimization)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (skipskip)
+		{
+			Assert(!prechecked);
+
+			if (key->sk_flags & SK_BT_SKIP)
+				continue;
+		}
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
@@ -5193,7 +5210,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
-									 continuescan))
+									 skipskip, continuescan))
 				continue;
 			return false;
 		}
@@ -5248,7 +5265,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -5266,7 +5283,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -5335,7 +5352,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
  */
 static bool
 _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
-					 TupleDesc tupdesc, ScanDirection dir, bool *continuescan)
+					 TupleDesc tupdesc, ScanDirection dir, bool skipskip,
+					 bool *continuescan)
 {
 	ScanKey		subkey = (ScanKey) DatumGetPointer(skey->sk_argument);
 	int32		cmpresult = 0;
@@ -5375,7 +5393,11 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 
 		if (isNull)
 		{
-			if (subkey->sk_flags & SK_BT_NULLS_FIRST)
+			if (skipskip)
+			{
+				/* treat scan key as non-required during skipskip calls */
+			}
+			else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -5428,8 +5450,12 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			 */
 			if (subkey != (ScanKey) DatumGetPointer(skey->sk_argument))
 				subkey--;
-			if ((subkey->sk_flags & SK_BT_REQFWD) &&
-				ScanDirectionIsForward(dir))
+			if (skipskip)
+			{
+				/* treat scan key as non-required during skipskip calls */
+			}
+			else if ((subkey->sk_flags & SK_BT_REQFWD) &&
+					 ScanDirectionIsForward(dir))
 				*continuescan = false;
 			else if ((subkey->sk_flags & SK_BT_REQBKWD) &&
 					 ScanDirectionIsBackward(dir))
@@ -5481,7 +5507,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			break;
 	}
 
-	if (!result)
+	if (!result && !skipskip)
 	{
 		/*
 		 * Tuple fails this qual.  If it's a required qual for the current
@@ -5525,6 +5551,8 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	Assert(!pstate->skipskip);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
@@ -5585,6 +5613,99 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	}
 }
 
+/*
+ * Can _bt_checkkeys/_bt_check_compare apply the 'skipskip' optimization?
+ *
+ * Return value indicates if the optimization is safe for the tuples on the
+ * page after caller's tuple, but before its page's finaltup.
+ */
+bool
+_bt_checkkeys_skipskip(IndexScanDesc scan, BTReadPageState *pstate,
+					   IndexTuple tuple, TupleDesc tupdesc)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	Relation	rel = scan->indexRelation;
+	ScanDirection dir = so->currPos.dir;
+	IndexTuple	finaltup = pstate->finaltup;
+	int			arrayidx = 0,
+				nfinaltupatts = 0;
+	bool		rangearrayseen = false;
+
+	Assert(!BTreeTupleIsPivot(tuple));
+	Assert(tuple != finaltup);
+
+	if (finaltup)
+		nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+	for (int ikey = 0; ikey < so->numberOfKeys; ikey++)
+	{
+		ScanKey		cur = so->keyData + ikey;
+		BTArrayKeyInfo *array = NULL;
+		Datum		tupdatum;
+		bool		tupnull;
+		int32		result;
+
+		/*
+		 * Only need to check range skip arrays within this loop.
+		 *
+		 * A SAOP array can always be treated as a non-required array within
+		 * _bt_check_compare.  A skip array without a lower or upper bound is
+		 * always safe to skip within _bt_check_compare, since it is satisfied
+		 * by every possible value.
+		 */
+		if (cur->sk_strategy != BTEqualStrategyNumber)
+			continue;
+		if (!(cur->sk_flags & SK_SEARCHARRAY))
+			continue;
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == ikey);
+		if (array->num_elems != -1 || array->null_elem)
+			continue;
+
+		/*
+		 * Found a range skip array to test.
+		 *
+		 * Scans with more than one range skip array are not eligible to use
+		 * the optimization.  Note that we support the skipskip optimization
+		 * for a qual like "WHERE a BETWEEN 1 AND 10 AND b BETWEEN 1 AND 3",
+		 * since there the qual actually requires only a single skip array.
+		 * However, if such a qual ended with "... AND C > 42", then it will
+		 * prevent use of the skipskip optimization.
+		 */
+		if (rangearrayseen)
+			return false;
+
+		/*
+		 * Don't attempt the optimization when we have a skip array and are
+		 * reading the rightmost leaf page (or the leftmost leaf page, when
+		 * scanning backwards)
+		 */
+		if (!finaltup)
+			return false;
+		rangearrayseen = true;
+
+		/* test the tuple that just advanced arrays within our caller */
+		Assert(cur->sk_flags & SK_BT_SKIP);
+		Assert(cur->sk_flags & SK_BT_REQFWD);
+		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], false, dir,
+								   tupdatum, tupnull, array, cur, &result);
+		if (result != 0)
+			return false;
+
+		/* test the page's finaltup iff relevant attribute isn't truncated */
+		if (cur->sk_attno > nfinaltupatts)
+			continue;
+
+		tupdatum = index_getattr(finaltup, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], false, dir,
+								   tupdatum, tupnull, array, cur, &result);
+		if (result != 0)
+			return false;
+	}
+
+	return true;
+}
+
 /*
  * _bt_killitems - set LP_DEAD state for items an indexscan caller has
  * told us were killed
-- 
2.45.2

v20-0002-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v20-0002-Add-skip-scan-to-nbtree.patchDownload
From f7b69e4854527d4e07d209eda05285233e8b7b6c Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v20 2/3] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
most useful in cases where the total number of distinct values in the
column 'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values are conceptually just like any
other value that might appear in an array, though they never actually
find exact matches in any index tuple.

Indexed columns that are only constrained by inequality strategy scan
keys also get skip arrays -- these are range skip arrays.  They are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".

Preprocessing is optimistic about skipping working out: it applies
simple static rules to decide where to place skip arrays.  It's up to
array-related scan code to keep the overhead of maintaining the scan's
skip arrays under control when it turns out that skipping isn't helpful.
A later commit that'll lower the overhead of maintaining skip arrays
will help with this by improving performance in the worst case.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |    3 +-
 src/include/access/nbtree.h                   |   29 +-
 src/include/catalog/pg_amproc.dat             |   22 +
 src/include/catalog/pg_proc.dat               |   27 +
 src/include/storage/lwlock.h                  |    1 +
 src/include/utils/skipsupport.h               |  101 +
 src/backend/access/index/indexam.c            |    3 +-
 src/backend/access/nbtree/nbtcompare.c        |  273 +++
 src/backend/access/nbtree/nbtree.c            |  215 ++-
 src/backend/access/nbtree/nbtsearch.c         |   95 +-
 src/backend/access/nbtree/nbtutils.c          | 1693 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |    4 +
 src/backend/commands/opclasscmds.c            |   25 +
 src/backend/storage/lmgr/lwlock.c             |    1 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/date.c                  |   46 +
 src/backend/utils/adt/meson.build             |    1 +
 src/backend/utils/adt/selfuncs.c              |  394 ++--
 src/backend/utils/adt/skipsupport.c           |   60 +
 src/backend/utils/adt/timestamp.c             |   48 +
 src/backend/utils/adt/uuid.c                  |   70 +
 src/backend/utils/misc/guc_tables.c           |   23 +
 doc/src/sgml/btree.sgml                       |   34 +-
 doc/src/sgml/indexam.sgml                     |    3 +-
 doc/src/sgml/indices.sgml                     |   49 +-
 doc/src/sgml/xindex.sgml                      |   16 +-
 src/test/regress/expected/alter_generic.out   |   10 +-
 src/test/regress/expected/create_index.out    |    4 +-
 src/test/regress/expected/join.out            |   30 +-
 src/test/regress/expected/psql.out            |    3 +-
 src/test/regress/expected/union.out           |   15 +-
 src/test/regress/sql/alter_generic.sql        |    5 +-
 src/test/regress/sql/create_index.sql         |    4 +-
 src/tools/pgindent/typedefs.list              |    3 +
 35 files changed, 2978 insertions(+), 334 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index fb94b3d1a..5046b31f3 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -206,7 +206,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index b88bd4435..9c53ba7e9 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -709,7 +710,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1022,10 +1024,22 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupportData sksup;		/* skip scan support (unless !use_sksup) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo   *low_order;		/* low_compare's ORDER proc */
+	FmgrInfo   *high_order;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1037,6 +1051,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Last array advancement matched -inf attr? */
 	bool		oppositeDirCheck;	/* explicit scanBehind recheck needed? */
@@ -1113,6 +1128,10 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on omitted prefix column */
+#define SK_BT_MINMAXVAL	0x00080000	/* lowest/highest key in array's range */
+#define SK_BT_NEXT		0x00100000	/* positions scan > sk_argument */
+#define SK_BT_PRIOR		0x00200000	/* positions scan < sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1149,6 +1168,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1159,7 +1182,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index b334a5fb8..fcf041d23 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b37e8a6f8..52ef0db1f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2260,6 +2275,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4447,6 +4465,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6303,6 +6324,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9345,6 +9369,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 2aa46fd50..6803d5b9e 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..6de3d82ec
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,101 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining pairs of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code falls back on next-key sentinel values for any opclass that
+ * doesn't provide its own skip support function.  There is no benefit to
+ * providing skip support for an opclass on a type where guessing that the
+ * next indexed value is the next possible indexable value seldom works out.
+ * It might not even be feasible to add skip support for a continuous type.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order).
+	 * This gives the B-Tree code a useful value to start from, before any
+	 * data has been read from the index.  These are also sometimes used to
+	 * detect when the scan has run out of indexable values to search for.
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 *
+	 * Operator class decrement/increment functions never have to directly
+	 * deal with the value NULL, nor with ASC vs. DESC column ordering.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 8b1f55543..0e62d7a7a 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 291cb8fc1..4da5a3c1d 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 6345a37a8..0e19fb620 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -29,6 +29,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -70,7 +71,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -78,11 +79,24 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 *
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -409,6 +423,7 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 		memcpy(scan->keyData, scankey, scan->numberOfKeys * sizeof(ScanKeyData));
 	so->numberOfKeys = 0;		/* until _bt_preprocess_keys sets it */
 	so->numArrayKeys = 0;		/* ditto */
+	so->skipScan = false;		/* tracks if skip arrays are in use */
 }
 
 /*
@@ -535,10 +550,159 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume all input scankeys will be output with its own
+	 * SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * scan array (and associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		CompactAttribute *attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescCompactAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		CompactAttribute *attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array/skip scan key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_MINMAXVAL)
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		attr = TupleDescCompactAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   attr->attbyval, attr->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+		CompactAttribute *attr;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array/skip scan key, while freeing memory allocated
+		 * for old sk_argument where required
+		 */
+		attr = TupleDescCompactAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+		if (!attr->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_MINMAXVAL)
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -549,7 +713,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -572,16 +737,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -608,6 +773,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -652,7 +818,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -674,14 +840,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -719,7 +880,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -763,11 +924,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -806,7 +967,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -815,7 +976,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -833,6 +994,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -842,7 +1004,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -852,14 +1014,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 9609bbda9..d332549cb 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -984,6 +984,13 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  Skip
+	 * array keys are = keys, though we'll sometimes need to treat the key as
+	 * if it was some kind of inequality instead.  This is required whenever
+	 * the skip array's current array element is a special sentinel value.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1059,8 +1066,46 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is MINMAXVAL, just choose low_compare/high_compare
+				 * (or consider if they imply a NOT NULL constraint).
+				 *
+				 * Note: if the array's low_compare key makes 'chosen' NULL,
+				 * then we behave as if the array's first element is -inf,
+				 * unless high_compare implies a usable NOT NULL constraint.
+				 * (It works the other way around during backwards scans.)
+				 */
+				if (chosen && (chosen->sk_flags & SK_BT_MINMAXVAL))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skiparraysk = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					Assert(so->skipScan);
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					if (!array->null_elem)
+						impliesNN = skiparraysk;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1097,6 +1142,40 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					strat_total == BTLessStrategyNumber)
 					break;
 
+				/*
+				 * If this is a scan key for a skip array whose current
+				 * element is marked NEXT or PRIOR, adjust strat_total
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(so->skipScan);
+					Assert(strat_total == BTEqualStrategyNumber);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[].
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).
+					 */
+					break;
+				}
+
 				/*
 				 * Done if that was the last attribute, or if next key is not
 				 * in sequence (implying no boundary key is available for the
@@ -1589,10 +1668,12 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * corresponding value from the last item on the page.  So checking with
 	 * the last item on the page would give a more precise answer.
 	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
+	 * We don't do this for the first page read by each (primitive) scan, to
+	 * avoid slowing down point queries.  They typically don't stand to gain
+	 * much when the optimization can be applied, and are more likely to
+	 * notice the overhead of the precheck.  Also avoid it during skip scans,
+	 * where individual primitive scans that don't just read one leaf page are
+	 * likely to advance their skip array(s) several times per leaf page read.
 	 *
 	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
 	 * just set a low-order required array's key to the best available match
@@ -1616,7 +1697,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!firstPage && !so->scanBehind && minoff < maxoff)
+	if (!firstPage && !so->skipScan && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 268b7b02a..ea8b34049 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -29,9 +29,37 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	Oid			eq_op;			/* = op to be used to add array, if any */
+} BTSkipPreproc;
+
 typedef struct BTSortArrayContext
 {
 	FmgrInfo   *sortproc;
@@ -63,16 +91,46 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
+static int	_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+										  int *numSkipArrayKeys);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey,
+									 FmgrInfo *orderprocp,
+									 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk,
+									ScanKey skey, FmgrInfo *orderprocp,
+									BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_decrement(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
+static void _bt_skip_preproc_strat_increment(IndexScanDesc scan,
+											 ScanKey arraysk,
+											 BTArrayKeyInfo *array);
 static int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 								   bool cur_elem_trig, ScanDirection dir,
 								   Datum tupdatum, bool tupnull,
 								   BTArrayKeyInfo *array, ScanKey cur,
 								   int32 *set_elem_result);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -256,6 +314,12 @@ _bt_freestack(BTStack stack)
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -271,10 +335,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -282,30 +348,25 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_preprocess_num_array_keys(scan, skipatts,
+												 &numSkipArrayKeys);
+	so->skipScan = (numSkipArrayKeys > 0);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys described within skipatts[]
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -320,17 +381,18 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/* Process input keys, and emit skip array keys described by skipatts[] */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -346,19 +408,97 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and its scan key where indicated */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* attribute already had an "=" key on input */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[numArrayKeyData];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].low_order = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].high_order = NULL;	/* for now */
+
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how this
+			 * constructed "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			numArrayKeyData++;	/* keep this scan key/array */
+		}
+
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
+		cur = &arrayKeyData[numArrayKeyData];
 		*cur = scan->keyData[input_ikey];
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -414,7 +554,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -425,7 +565,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -442,7 +582,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -517,23 +657,232 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].low_order = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].high_order = NULL;	/* unused */
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
 	return arrayKeyData;
 }
 
+/*
+ *	_bt_preprocess_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit all required so->arrayKeys[] entries.
+ *
+ * Also sets *numSkipArrayKeys to the number of skip arrays that caller must
+ * make sure to add to the scan keys it'll output (complete with corresponding
+ * so->arrayKeys[] entries), and sets the corresponding attributes positions
+ * in *skipatts argument.  Every attribute that receives a skip array will
+ * have its skip support routine set in *skipatts, too.
+ */
+static int
+_bt_preprocess_num_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+							  int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_inkey = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make use of its 'firstmatch' optimization.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when later preprocessing deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				(*numSkipArrayKeys)++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				break;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required later on.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatt
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatt->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										 BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack even an equality strategy
+	 * operator, but they're still supported.  Cope by making caller not use a
+	 * skip array for this index column (nor for any lower-order columns).
+	 */
+	if (!OidIsValid(skipatt->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatt->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+													   reverse,
+													   &skipatt->sksup);
+
+	/* might not have set up skip support, but can skip either way */
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys_final() -- fix up array scan key references
  *
@@ -633,7 +982,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -669,6 +1019,14 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && !array->null_elem)
+						_bt_skip_preproc_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
@@ -684,7 +1042,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -980,26 +1338,28 @@ _bt_merge_arrays(IndexScanDesc scan, ScanKey skey, FmgrInfo *sortproc,
  * guaranteeing that at least the scalar scan key can be considered redundant.
  * We return false if the comparison could not be made (caller must keep both
  * scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar inequality scan keys.
  */
 static bool
 _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
 	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
 		   skey->sk_strategy != BTEqualStrategyNumber);
@@ -1009,8 +1369,7 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1041,11 +1400,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensures that the scalar scan key can be eliminated as redundant
+		 */
+		eliminated = true;
+
+		_bt_array_preproc_shrink(arraysk, skey, orderprocp, array, qual_ok);
+	}
+	else
+	{
+		/*
+		 * With a skip array, it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when there's no cross-type support for comparing skey
+		 * to an existing inequality that's already associated with the array.
+		 */
+		eliminated = _bt_skip_preproc_shrink(scan, arraysk, skey, orderprocp,
+											 array, qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when
+ * determining its redundancy against a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_preproc_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+						 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1097,10 +1510,292 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of a skip array scan key when determining its
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_preproc_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array.  This forces the array's elements to be generated within the limits
+ * of a range that's described/constrained by the array's inequalities.
+ *
+ * Return value indicates if the scalar scan key could be "eliminated".  We
+ * return true in the common case where caller's scan key was successfully
+ * rolled into the skip array.  We return false when we can't do that due to
+ * the presence of an existing, conflicting inequality that cannot be compared
+ * to caller's inequality due to a lack of suitable cross-type support.
+ */
+static bool
+_bt_skip_preproc_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+						FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+						bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way scan code can generally assume that
+	 * it'll always be safe to use the ORDER procs stored in so->orderProcs[].
+	 * The only exception is cases where the array's scan key is MINMAXVAL.
+	 *
+	 * When the array's scan key is marked MINMAXVAL, then it'll lack a valid
+	 * sk_argument.  The scan must apply the array's low_compare and/or
+	 * high_compare (if any) to establish whether a given tupdatum is within
+	 * the range of the skip array.  This could involve applying the 3-way
+	 * low_order/high_order ORDER proc we store here (though never the ORDER
+	 * proc stored in so->orderProcs[]).
+	 *
+	 * Note: we sometimes use low_compare/high_compare inequalities with the
+	 * array's current element value, and with values that were mutated by the
+	 * array's skip support routine.  A skip array must always use its index
+	 * attribute's own input opclass/type, so this will always work correctly.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (!array->high_compare)
+			{
+				/* array currently lacks a high_compare, make space for one */
+				array->high_compare = palloc(sizeof(ScanKeyData));
+				array->high_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare, keep old one */
+
+				/* replace old high_compare with caller's new one */
+			}
+
+			/* caller's high_compare becomes skip array's high_compare */
+			*array->high_compare = *skey;
+			*array->high_order = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (!array->low_compare)
+			{
+				/* array currently lacks a low_compare, make space for one */
+				array->low_compare = palloc(sizeof(ScanKeyData));
+				array->low_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare, keep old one */
+
+				/* replace old low_compare with caller's new one */
+			}
+
+			/* caller's low_compare becomes skip array's low_compare */
+			*array->low_compare = *skey;
+			*array->low_order = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
 
+/*
+ * Perform final preprocessing for a skip array.  Called when the skip array's
+ * final low_compare and high_compare have been chosen.
+ *
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts the
+ * operator strategies).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value MINMAXVAL, using >= instead of using >
+ * (or using <= instead of < during backwards scans) makes it safe to include
+ * lower-order scan keys in the insertion scan key (there must be lower-order
+ * scan keys after the skip array).  We will avoid an extra _bt_first to find
+ * the first value in the index > sk_argument, at least when the first real
+ * matching value in the index happens to be an exact match for the newly
+ * transformed (newly incremented/decremented) sk_argument value.
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01' -- which would be wrong.
+ */
+static void
+_bt_skip_preproc_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+	Assert(!array->null_elem);
+
+	/* If skip array doesn't have skip support, can't apply optimization */
+	if (!array->use_sksup)
+		return;
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skip_preproc_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skip_preproc_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skip_preproc_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+	Assert(array->use_sksup);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup.decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skip_preproc_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+								 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+	Assert(array->use_sksup);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup.increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Have all we need to transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  * qsort_arg comparator for sorting array elements
  */
@@ -1220,6 +1915,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -1342,6 +2039,98 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, because the array doesn't really
+ * have any elements (it generates its array elements procedurally instead).
+ * Note that this may include a NULL value/an IS NULL qual.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ *
+ * Note: This function doesn't actually binary search.  It uses an interface
+ * that's as similar to _bt_binsrch_array_skey as possible for consistency.
+ * "Binary searching a skip array" just means determining if tupdatum/tupnull
+ * are before, within, or beyond the range of caller's skip array.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -1351,29 +2140,486 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	CompactAttribute *attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescCompactAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting skip array to lowest element, which isn't just NULL */
+		if (array->use_sksup && !array->low_compare)
+			skey->sk_argument = datumCopy(array->sksup.low_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_MINMAXVAL;
+	}
+	else
+	{
+		/* Setting skip array to highest element, which isn't just NULL */
+		if (array->use_sksup && !array->high_compare)
+			skey->sk_argument = datumCopy(array->sksup.high_elem,
+										  attr->attbyval, attr->attlen);
+		else
+			skey->sk_flags |= SK_BT_MINMAXVAL;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	CompactAttribute *attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescCompactAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, attr->attbyval, attr->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	CompactAttribute *attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	attr = TupleDescCompactAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  attr->attbyval, attr->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  attr->attbyval, attr->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	CompactAttribute *attr;
+
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	attr = TupleDescCompactAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+	CompactAttribute *attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_MINMAXVAL));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the minimum value within the range
+	 * of a skip array (often just -inf) is never decrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINMAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element is outside of the range of the array.
+		 *
+		 * This is only possible if low_compare represents an >= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the prior value cannot possibly satisfy low_compare, so we can
+		 * give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(array->low_order,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Decrement by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &uflow);
+	if (unlikely(uflow))
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	attr = TupleDescCompactAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+	CompactAttribute *attr;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_MINMAXVAL));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the maximum value within the range
+	 * of a skip array (often just +inf) is never incrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINMAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element is outside of the range of the array.
+		 *
+		 * This is only possible if high_compare represents an <= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the next value cannot possibly satisfy high_compare, so we can
+		 * give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(array->high_order,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Increment by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &oflow);
+	if (unlikely(oflow))
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	attr = TupleDescCompactAttr(RelationGetDescr(rel), skey->sk_attno - 1);
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!attr->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!attr->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -1389,6 +2635,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -1398,29 +2645,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -1480,6 +2728,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -1487,7 +2736,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -1499,16 +2747,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -1633,9 +2875,100 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_MINMAXVAL))
+		{
+			/* Scankey has a valid/comparable sk_argument value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0 && (cur->sk_flags & SK_BT_NEXT))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument + infinitesimal"
+				 */
+				if (ScanDirectionIsForward(dir))
+				{
+					/* tupdatum is still < "sk_argument + infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was forward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to backward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum as its current element.
+				 */
+				return false;
+			}
+			else if (result == 0 && (cur->sk_flags & SK_BT_PRIOR))
+			{
+				/*
+				 * tupdatum is == sk_argument, but true current array element
+				 * is actually "sk_argument - infinitesimal"
+				 */
+				if (ScanDirectionIsBackward(dir))
+				{
+					/* tupdatum is still > "sk_argument - infinitesimal" */
+					return true;
+				}
+
+				/*
+				 * Scan direction was backward during the scan's last call to
+				 * _bt_advance_array_keys, but has since changed to forward.
+				 * It's time for caller to advance the scan's array keys.
+				 * Array goes back to having tupdatum as its current element.
+				 */
+				return false;
+			}
+		}
+		else
+		{
+			/*
+			 * Current array element/array = scan key value is a sentinel
+			 * value that represents the lowest (or highest) possible value
+			 * that's still within the range of the array.
+			 *
+			 * We don't have a valid sk_argument value from = scan key.  Check
+			 * if tupdatum is within the range of skip array instead.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum satisfies both low_compare and high_compare, so
+				 * it's time to advance the array keys.
+				 *
+				 * Note: It's possible that the skip array will "advance" from
+				 * its current MINMAXVAL representation to an alternative
+				 * representation where the = key gets a useful sk_argument,
+				 * even though both representations are logically equivalent.
+				 * This is possible when low_compare uses the <= strategy, and
+				 * when high_compare uses the >= strategy.  Allowing two
+				 * distinct representations of the same array value keeps
+				 * things simple in scenarios involving cross-type operators.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1971,18 +3304,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -2007,18 +3331,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -2036,12 +3351,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -2117,11 +3441,63 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  "Binary searching" only determined whether
+		 * tupdatum is beyond, before, or within the range of the skip array.
+		 *
+		 * As always, we set the array element to its closest available match.
+		 * But unlike with a conventional array, a skip array's new element
+		 * might be MINMAXVAL, which represents the lowest (or the highest)
+		 * real value that is within the range of the skip array.
+		 */
+		Assert(so->skipScan);
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == "some array element".
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -2471,6 +3847,8 @@ end_toplevel_scan:
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -2483,10 +3861,16 @@ end_toplevel_scan:
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never generated skip array scan keys, it would be possible for "gaps"
+ * to appear that make it unsafe to mark any subsequent input scan keys
+ * (copied from scan->keyData[]) as required to continue the scan.  Prior to
+ * Postgres 18, a qual like "WHERE y = 4" always required a full index scan.
+ * It'll now become "WHERE x = ANY('{every possible x value}') and y = 4" on
+ * output, due to the addition of a skip array on "x".  This has the potential
+ * to be much more efficient than a full index scan (though it'll behave like
+ * a full index scan when there are many distinct values for "x").
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -2523,7 +3907,12 @@ end_toplevel_scan:
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -2587,6 +3976,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProcs[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -2618,6 +4015,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
+		Assert(!so->skipScan);
 
 		return;
 	}
@@ -2675,7 +4073,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -2732,7 +4131,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -2780,6 +4178,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, _bt_preprocess_array_keys has already added "=" keys
+			 * sufficient to form an unbroken series of "=" constraints on all
+			 * attrs prior to the attr from the final scan->keyData[] key.
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -2868,6 +4271,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -2876,6 +4280,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -2895,8 +4300,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -3038,10 +4441,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -3107,6 +4511,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -3116,6 +4523,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -3189,6 +4612,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -3750,6 +5174,21 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key uses one of several sentinel values (usually
+		 * only at the start or end of each primitive index scan).  We always
+		 * fall back on _bt_tuple_before_array_skeys to decide what to do.
+		 */
+		if (key->sk_flags & (SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index b87c959a2..c2dd458c8 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 2c325badf..303622f9d 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 2f558ffea..39972dd0e 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0b53cba80..fa82328ef 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -370,6 +370,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index f279853de..4227ab1a7 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f23cfad71..244f48f4f 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 93e4a8906..2d44f0161 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -193,6 +193,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -214,6 +216,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5759,6 +5763,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6817,6 +6907,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6826,17 +6963,22 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6852,14 +6994,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -6869,13 +7016,90 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't absord a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6897,6 +7121,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6917,7 +7142,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6933,6 +7158,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6948,6 +7205,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -6976,7 +7234,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * natural ceiling on the worst case number of descents -- there
 		 * cannot possibly be more than one descent per leaf page scanned.
 		 *
-		 * Clamp the number of descents to at most 1/3 the number of index
+		 * Clamp the number of descents to at most the total number of index
 		 * pages.  This avoids implausibly high estimates with low selectivity
 		 * paths, where scans usually require only one or two descents.  This
 		 * is most likely to help when there are several SAOP clauses, where
@@ -6984,18 +7242,17 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * array elements as the number of descents would frequently lead to
 		 * wild overestimates.
 		 *
-		 * We somewhat arbitrarily don't just make the cutoff the total number
-		 * of leaf pages (we make it 1/3 the total number of pages instead) to
-		 * give the btree code credit for its ability to continue on the leaf
-		 * level with low selectivity scans.
+		 * Note: num_sa_scans is derived in a way that treats any skip scan
+		 * quals as if they were ScalarArrayOpExpr quals whose array has as
+		 * many distinct elements as possibly-matching values in the index.
 		 */
-		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
+		num_sa_scans = Min(num_sa_scans, index->pages);
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
 		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
-		 * to integer.
+		 * ScalarArrayOpExpr quals (as well as any skip scan quals) included
+		 * in indexBoundQuals, and then round to integer.
 		 *
 		 * It is tempting to make genericcostestimate behave as if SAOP
 		 * clauses work in almost the same way as scalar operators during
@@ -7056,104 +7313,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..90ddfd314
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index ba9bae050..f64fbf263 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2286,6 +2287,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 4f8402ef9..1b72059d2 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,6 +13,7 @@
 
 #include "postgres.h"
 
+#include <limits.h>
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
@@ -21,6 +22,7 @@
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -416,6 +418,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 22f16a3b4..bb769ff92 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1761,6 +1762,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3609,6 +3621,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 1904eb65b..22380abae 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2248,7 +2248,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2268,7 +2268,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 079fcf46f..ac029e641 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4527,24 +4527,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -8070,9 +8071,10 @@ where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1 and j2.id1 >= any (array[1,5]);
    Merge Cond: (j1.id1 = j2.id1)
    Join Filter: (j2.id2 = j1.id2)
    ->  Index Scan using j1_id1_idx on j1
-   ->  Index Scan using j2_id1_idx on j2
+   ->  Index Only Scan using j2_pkey on j2
          Index Cond: (id1 >= ANY ('{1,5}'::integer[]))
-(6 rows)
+         Filter: ((id1 % 1000) = 1)
+(7 rows)
 
 select * from j1
 inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 36dc31c16..aef239200 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5240,9 +5240,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index caa8fe70a..96962817e 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1472,18 +1472,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index c085e05f0..6e3b9f123 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -858,7 +858,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -868,7 +868,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e1c4f913f..1b7a9ce41 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -218,6 +218,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2674,6 +2675,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.45.2

In reply to: Peter Geoghegan (#57)
3 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Jan 3, 2025 at 2:43 PM Peter Geoghegan <pg@bowt.ie> wrote:

I now attach v20.

Attached is v21. This revision is just to fix bitrot against HEAD that
was caused by recent commits of mine -- all of which were related to
nbtree preprocessing.

Now that nbtree has a separate file for preprocessing related code
(nbtpreprocesskeys.c), it's easier to see how the code added by the
skip scan patch fits together with everything else. The patch
actually adds slightly more code to nbtpreprocesskeys.c (to
preprocessing scan keys in new ways) than it adds to nbtutils.c (to
evaluate those same scan keys in new ways). This new structure
definitely improves readability.

--
Peter Geoghegan

Attachments:

v21-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v21-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From 5e48d7e64630913876b306f9d0ac11364defb86f Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v21 1/3] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 contrib/bloom/blscan.c                        |   1 +
 doc/src/sgml/bloom.sgml                       |   7 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 22 files changed, 244 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index dc6e01842..0d1ad8379 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -147,6 +147,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 9a9845475..0461022ef 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -585,6 +585,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index 7d1e66152..81440a283 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index cc40e928e..609e85fda 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index a3a1fccf3..c4f730437 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 07bae342e..369e39bc4 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 3d617f168..6345a37a8 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -69,6 +69,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -552,6 +553,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -578,6 +580,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -705,6 +708,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			Assert(btscan->btps_nextScanPage != P_NONE);
 			*next_scan_page = btscan->btps_nextScanPage;
@@ -805,6 +813,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -839,6 +849,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 472ce06f1..941b4eaaf 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -950,6 +950,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 986362a77..991f99b27 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c24e66f82..6b65037cd 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2105,6 +2107,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2118,6 +2121,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2134,6 +2138,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2652,6 +2657,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index bf801fe78..472169d61 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -116,6 +116,7 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++)
 	{
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 6a8a60b8c..4cddfaae1 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -174,9 +174,10 @@ CREATE INDEX
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..178436.00 rows=1 width=0) (actual time=20.005..20.005 rows=2300 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
          Buffers: shared hit=19608
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 22.632 ms
-(10 rows)
+(11 rows)
 </programlisting>
   </para>
 
@@ -209,12 +210,14 @@ CREATE INDEX
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..4.52 rows=11 width=0) (actual time=0.026..0.026 rows=7 loops=1)
                Index Cond: (i5 = 123451)
                Buffers: shared hit=3
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..4.52 rows=11 width=0) (actual time=0.007..0.007 rows=8 loops=1)
                Index Cond: (i2 = 898732)
                Buffers: shared hit=3
+               Index Searches: 1
  Planning Time: 0.264 ms
  Execution Time: 0.047 ms
-(13 rows)
+(15 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index d0d176cc5..23e88ca0b 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4198,12 +4198,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aab..34225c010 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -730,9 +730,11 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
                Buffers: shared hit=2
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
          Buffers: shared hit=24 read=6
+         Index Searches: 1
  Planning:
    Buffers: shared hit=15 dirtied=9
  Planning Time: 0.485 ms
@@ -791,6 +793,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
                            Buffers: shared hit=2
+                           Index Searches: 1
  Planning:
    Buffers: shared hit=12
  Planning Time: 0.187 ms
@@ -860,6 +863,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
    Buffers: shared hit=1
  Planning Time: 0.039 ms
@@ -894,8 +898,10 @@ EXPLAIN (ANALYZE, BUFFERS OFF) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND un
    -&gt;  BitmapAnd  (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
  Planning Time: 0.162 ms
  Execution Time: 0.143 ms
 </screen>
@@ -924,6 +930,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
                Buffers: shared read=2
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1059,6 +1066,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
    Buffers: shared hit=16
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
          Buffers: shared hit=16
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index 6361a14e6..6fc9bfedc 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -506,9 +506,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
          Buffers: shared hit=4
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(9 rows)
+(10 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 7a928bd7b..7a00e4c0e 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index f2d146581..a5df4107a 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 5ecf971da..d9df5c0e1 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index c52bc40e8..b053891b6 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2340,6 +2340,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2657,47 +2661,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2713,16 +2726,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2741,7 +2757,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2757,16 +2773,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2787,7 +2806,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2858,16 +2877,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2875,17 +2897,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2961,17 +2986,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -2982,17 +3013,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3027,17 +3064,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3048,17 +3091,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3112,17 +3161,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3144,17 +3199,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3482,12 +3543,14 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3503,9 +3566,10 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3553,13 +3617,17 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4129,14 +4197,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 88911ca2b..cde2eac73 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index d5aab4e56..2197b0ac5 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index d67598d5c..5492bd6e9 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -573,6 +573,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.47.1

v21-0003-Lower-the-overhead-of-nbtree-runtime-skip-checks.patchapplication/octet-stream; name=v21-0003-Lower-the-overhead-of-nbtree-runtime-skip-checks.patchDownload
From 4eea74961ab80a2cbe6b975b0cc303e0e68b183c Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v21 3/3] Lower the overhead of nbtree runtime skip checks.

Add a new "skipskip" strategy to fix regressions in index scans that are
nominally eligible to use skip scan, but can never actually benefit from
skipping.  These are cases where a leading prefix column contains many
distinct values -- typically as many distinct values are there are total
index tuples.  This works by dynamically falling back on a strategy that
temporarily treats all user-supplied scan keys as if they were marked
non-required, while avoiding all skip array maintenance.  The new
optimization is applied for all pages beyond each primitive index scan's
first leaf page read.

Note that this commit doesn't actually change anything about when or how
skip scan decides to schedule new primitive index scans.  It is limited
to saving CPU cycles by varying how we read individual index tuples in
cases where maintaining skip arrays cannot possibly pay for itself.

Fixing (or significantly ameliorating) regressions in the worst case
like this enables skip scan's approach within the planner.  The planner
doesn't generate distinct index paths to represent nbtree index skip
scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  The planner considers skipping when
costing relevant index paths, but that in itself won't influence skip
scan's runtime behavior.

This approach makes skip scan adapt to skewed key distributions.  It
also makes planning less sensitive to misestimations.  An index path
with an inaccurate estimate of the total number of required primitive
index scans won't have to perform many more primitive scans at runtime
(it'll behave like a traditional full index scan instead).

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h           |   3 +
 src/backend/access/nbtree/nbtsearch.c |  40 +++++
 src/backend/access/nbtree/nbtutils.c  | 238 +++++++++++++++++++++-----
 3 files changed, 242 insertions(+), 39 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 4aafc2bc2..1401ded7f 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1122,6 +1122,8 @@ typedef struct BTReadPageState
 	 */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
+	bool		skipskip;		/* skip maintenance of skip arrays? */
+	int			ikey;			/* Start comparisons from ikey'th scan key */
 
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
@@ -1326,6 +1328,7 @@ extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arra
 						  IndexTuple tuple, int tupnatts);
 extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 								  IndexTuple finaltup);
+extern void _bt_checkkeys_skipskip(IndexScanDesc scan, BTReadPageState *pstate);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index fef84db86..c230f9377 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1642,6 +1642,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.continuescan = true; /* default assumption */
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
+	pstate.skipskip = false;
+	pstate.ikey = 0;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
@@ -1734,6 +1736,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 
 				/* Deliberately don't unset scanBehind flag just yet */
 			}
+
+			/*
+			 * Skip maintenance of skip arrays (if any) during primitive index
+			 * scans that read leaf pages after the first
+			 */
+			if (so->skipScan && !firstPage && minoff < maxoff)
+				_bt_checkkeys_skipskip(scan, &pstate);
 		}
 
 		/* load items[] in ascending order */
@@ -1772,6 +1781,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum < pstate.skip);
+				Assert(!pstate.skipskip);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1837,6 +1847,17 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
+			if (pstate.skipskip)
+			{
+				/*
+				 * reset array keys for finaltup call, since skipskip
+				 * optimization prevented ordinary array maintenance
+				 */
+				Assert(so->skipScan);
+				_bt_start_array_keys(scan, dir);
+				pstate.skipskip = false;
+				pstate.ikey = 0;
+			}
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -1856,6 +1877,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			ItemId		iid = PageGetItemId(page, minoff);
 
 			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+			/*
+			 * Skip maintenance of skip arrays (if any) during primitive index
+			 * scans that read leaf pages after the first
+			 */
+			if (so->skipScan && !firstPage && minoff < maxoff)
+				_bt_checkkeys_skipskip(scan, &pstate);
 		}
 
 		/* load items[] in descending order */
@@ -1897,6 +1925,17 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff && pstate.skipskip)
+			{
+				/*
+				 * reset array keys for finaltup call, since skipskip
+				 * optimization prevented ordinary array maintenance
+				 */
+				Assert(so->skipScan);
+				_bt_start_array_keys(scan, dir);
+				pstate.skipskip = false;
+				pstate.ikey = 0;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
@@ -1908,6 +1947,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum > pstate.skip);
+				Assert(!pstate.skipskip);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index e1632673c..ffb15cbaf 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -57,11 +57,12 @@ static bool _bt_verify_keys_with_arraykeys(IndexScanDesc scan);
 #endif
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool skipskip,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-								 ScanDirection dir, bool *continuescan);
+								 ScanDirection dir, bool skipskip, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -1429,14 +1430,14 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
  * postcondition's <= operator with a >=.  In other words, just swap the
  * precondition with the postcondition.)
  *
- * We also deal with "advancing" non-required arrays here.  Callers whose
- * sktrig scan key is non-required specify sktrig_required=false.  These calls
- * are the only exception to the general rule about always advancing the
- * required array keys (the scan may not even have a required array).  These
- * callers should just pass a NULL pstate (since there is never any question
- * of stopping the scan).  No call to _bt_tuple_before_array_skeys is required
- * ahead of these calls (it's already clear that any required scan keys must
- * be satisfied by caller's tuple).
+ * We also deal with "advancing" non-required arrays here (or arrays that are
+ * temporarily being treated as non-required due to the application of the
+ * "skipskip" optimization).  Callers whose sktrig scan key is non-required
+ * specify sktrig_required=false.  These calls are the only exception to the
+ * general rule about always advancing the required array keys (the scan may
+ * not even have a required array).  These callers should just pass a NULL
+ * pstate (since there is never any question of stopping the scan).  No call
+ * to _bt_tuple_before_array_skeys is required ahead of these calls.
  *
  * Note that we deal with non-array required equality strategy scan keys as
  * degenerate single element arrays here.  Obviously, they can never really
@@ -1493,8 +1494,8 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		pstate->firstmatch = false;
 
-		/* Shouldn't have to invalidate 'prechecked', though */
-		Assert(!pstate->prechecked);
+		/* Shouldn't have to invalidate 'prechecked' or 'skipskip' or 'ikey' */
+		Assert(!pstate->prechecked && !pstate->skipskip && pstate->ikey == 0);
 
 		/*
 		 * Once we return we'll have a new set of required array keys, so
@@ -1546,8 +1547,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -1694,7 +1693,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -1736,7 +1735,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -1751,7 +1750,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -1864,7 +1863,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -2271,13 +2270,14 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	TupleDesc	tupdesc = RelationGetDescr(scan->indexRelation);
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ScanDirection dir = so->currPos.dir;
-	int			ikey = 0;
+	int			ikey = pstate->ikey;
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+							arrayKeys, pstate->skipskip,
+							pstate->prechecked, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
@@ -2289,21 +2289,22 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
 		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
+		Assert(!pstate->skipskip && !pstate->prechecked &&
+			   !pstate->firstmatch);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
 	if (pstate->prechecked || pstate->firstmatch)
 	{
 		bool		dcontinuescan;
-		int			dikey = 0;
+		int			dikey = pstate->ikey;
 
 		/*
 		 * Call relied on continuescan/firstmatch prechecks -- assert that we
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, pstate->skipskip, false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -2326,6 +2327,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * It's also possible that the scan is still _before_ the _start_ of
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
+	Assert(!pstate->skipskip);
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
@@ -2434,7 +2436,7 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	Assert(so->numArrayKeys);
 
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -2473,17 +2475,24 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Though we advance non-required array keys on our own, that shouldn't have
  * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
+ * have no fixed relationship with the scan's progress.  (skipskip=true makes
+ * us treat required scan keys as non-required, though.  That's safe because
+ * _bt_readpage will account for our failure to fully maintain the scan's
+ * arrays later on, right before its finaltup call to _bt_checkkeys.)
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass skipskip=true to instruct us to skip all of the scan's skip arrays.
+ * This provides the scan with a way of keeping the cost of maintaining its
+ * skip arrays under control, given skip arrays on high cardinality columns
+ * (i.e. given a skip array on a column which isn't a good fit for skip scan).
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool skipskip,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -2500,10 +2509,18 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when reading a page that's now using the "skipskip" optimization)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (skipskip)
+		{
+			Assert(!prechecked);
+
+			if (key->sk_flags & SK_BT_SKIP)
+				continue;
+		}
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
@@ -2561,7 +2578,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
-									 continuescan))
+									 skipskip, continuescan))
 				continue;
 			return false;
 		}
@@ -2616,7 +2633,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -2634,7 +2651,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -2703,7 +2720,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
  */
 static bool
 _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
-					 TupleDesc tupdesc, ScanDirection dir, bool *continuescan)
+					 TupleDesc tupdesc, ScanDirection dir, bool skipskip,
+					 bool *continuescan)
 {
 	ScanKey		subkey = (ScanKey) DatumGetPointer(skey->sk_argument);
 	int32		cmpresult = 0;
@@ -2743,7 +2761,11 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 
 		if (isNull)
 		{
-			if (subkey->sk_flags & SK_BT_NULLS_FIRST)
+			if (skipskip)
+			{
+				/* treat scan key as non-required during skipskip calls */
+			}
+			else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -2797,8 +2819,12 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			 */
 			Assert(subkey != (ScanKey) DatumGetPointer(skey->sk_argument));
 			subkey--;
-			if ((subkey->sk_flags & SK_BT_REQFWD) &&
-				ScanDirectionIsForward(dir))
+			if (skipskip)
+			{
+				/* treat scan key as non-required during skipskip calls */
+			}
+			else if ((subkey->sk_flags & SK_BT_REQFWD) &&
+					 ScanDirectionIsForward(dir))
 				*continuescan = false;
 			else if ((subkey->sk_flags & SK_BT_REQBKWD) &&
 					 ScanDirectionIsBackward(dir))
@@ -2850,7 +2876,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			break;
 	}
 
-	if (!result)
+	if (!result && !skipskip)
 	{
 		/*
 		 * Tuple fails this qual.  If it's a required qual for the current
@@ -2894,6 +2920,8 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	Assert(!pstate->skipskip);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
@@ -2954,6 +2982,138 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	}
 }
 
+/*
+ * Consider apply _bt_checkkeys 'skipskip' optimization
+ *
+ * Sets pstate.skipskip when it's safe for _bt_readpage caller to apply the
+ * 'skipskip' optimization on this page during skip scans.
+ *
+ * If the 'skipskip' optimization is safe then we might also set pstate.ikey,
+ * which allows the _bt_checkkeys calls for the page to start at the first
+ * scan key that might be unsatisfied.  This provides an additional benefit in
+ * that it allows _bt_checkkeys to inexpensively skip over a prefix of
+ * ignorable keys.
+ */
+void
+_bt_checkkeys_skipskip(IndexScanDesc scan, BTReadPageState *pstate)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	ScanDirection dir = so->currPos.dir;
+	ItemId		iid;
+	IndexTuple	finaltup = pstate->finaltup,
+				firsttup;
+	int			arrayidx = 0,
+				nfinaltupatts,
+				firstunequalattnum;
+	bool		nonsharedprefix_nonskip = false,
+				nonsharedprefix_range_skip = false,
+				ikeyset = false;
+	int			set_ikey = 0;
+
+	/* Only called during skip scans */
+	Assert(so->skipScan && !pstate->skipskip);
+
+	/* Can't combine 'skipskip' with the similar 'precheck' optimization */
+	Assert(!pstate->prechecked);
+
+	Assert(pstate->minoff < pstate->maxoff);
+	if (ScanDirectionIsForward(dir))
+		iid = PageGetItemId(pstate->page, pstate->minoff);
+	else
+		iid = PageGetItemId(pstate->page, pstate->maxoff);
+	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	Assert(!BTreeTupleIsPivot(firsttup));
+	Assert(firsttup != finaltup);
+
+	nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+
+	firstunequalattnum = 1;
+	if (so->numberOfKeys > 2)
+		firstunequalattnum = _bt_keep_natts_fast(rel, firsttup, finaltup);
+
+	for (int ikey = 0; ikey < so->numberOfKeys; ikey++)
+	{
+		ScanKey		cur = so->keyData + ikey;
+		BTArrayKeyInfo *array = NULL;
+		Datum		tupdatum;
+		bool		tupnull;
+		int32		result;
+
+		if (cur->sk_attno >= firstunequalattnum)
+		{
+			if (!ikeyset || !nonsharedprefix_nonskip)
+			{
+				ikeyset = true;
+				set_ikey = ikey;
+			}
+			if (!(cur->sk_flags & SK_BT_SKIP))
+				nonsharedprefix_nonskip = true;
+		}
+
+		if (cur->sk_strategy != BTEqualStrategyNumber)
+			continue;
+		if (!(cur->sk_flags & SK_SEARCHARRAY))
+			continue;
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == ikey);
+
+		/*
+		 * Only need to maintain set_ikey and current so->arrayKeys[] offset,
+		 * unless dealing with a range skip array
+		 */
+		if (array->num_elems != -1 || array->null_elem)
+			continue;
+
+		/*
+		 * Found a range skip array to test.
+		 *
+		 * If this is the first range skip array, it is still safe to apply
+		 * the skipskip optimization.  If this is the second or subsequent
+		 * range skip array, then it is only safe if there is no more than one
+		 * range skip array on an attribute whose values change on this page.
+		 *
+		 * Note: we deliberately ignore regular (non-range) skip arrays, since
+		 * they're always satisfied by any possible attribute value.
+		 */
+		if (nonsharedprefix_range_skip)
+			return;
+		if (cur->sk_attno < firstunequalattnum)
+		{
+			/*
+			 * This range skip array doesn't count towards our "no more than
+			 * one range skip array" limit -- but it must still be satisfied
+			 * by both firsttup and finaltup
+			 */
+		}
+		else
+			nonsharedprefix_range_skip = true;
+
+		/* Test the first/lower bound non-pivot tuple on the page */
+		tupdatum = index_getattr(firsttup, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], false, dir,
+								   tupdatum, tupnull, array, cur, &result);
+		if (result != 0)
+			return;
+
+		/* Test the page's finaltup (unless attribute is truncated) */
+		if (cur->sk_attno <= nfinaltupatts)
+		{
+			tupdatum = index_getattr(finaltup, cur->sk_attno, tupdesc,
+									 &tupnull);
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], false, dir,
+									   tupdatum, tupnull, array, cur, &result);
+			if (result != 0)
+				return;
+		}
+	}
+
+	pstate->ikey = set_ikey;
+	pstate->skipskip = true;
+}
+
 /*
  * _bt_killitems - set LP_DEAD state for items an indexscan caller has
  * told us were killed
-- 
2.47.1

v21-0002-Add-skip-scan-to-nbtree.patchapplication/octet-stream; name=v21-0002-Add-skip-scan-to-nbtree.patchDownload
From 863e45f3904c392741b30bdfc2532ec108175e5a Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v21 2/3] Add skip scan to nbtree.

Skip scan allows nbtree index scans to efficiently use a composite index
on the columns (a, b) for queries with a qual "WHERE b = 5".  This is
most useful in cases where the total number of distinct values in the
column 'a' is reasonably small (think hundreds, possibly thousands).  In
effect, a skip scan treats the composite index on (a, b) as if it was a
series of disjunct subindexes -- one subindex per distinct 'a' value.

The scan exhaustively "searches every subindex" by using a qual that
behaves like "WHERE a = ANY(<every possible 'a' value>) AND b = 5".
This works by extending the design for arrays established by commit
5bf748b8.  "Skip arrays" generate their array values procedurally and
on-demand, but otherwise work just like conventional SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values are conceptually just like any
other value that might appear in an array, though they never actually
find exact matches in any index tuple.

Indexed columns that are only constrained by inequality strategy scan
keys also get skip arrays -- these are range skip arrays.  They are just
like conventional skip arrays, but only generate values from within a
given range.  The range is constrained by input inequality scan keys.
For example, a skip array on "a" can only ever use array element values
1 and 2 when generated for a qual "WHERE a BETWEEN 1 AND 2 AND b = 66".
Such a skip array works very much like the conventional SAOP array that
would be generated given the qual "WHERE a = ANY('{1, 2}') AND b = 66".

Preprocessing is optimistic about skipping working out: it applies
simple static rules to decide where to place skip arrays.  It's up to
array-related scan code to keep the overhead of maintaining the scan's
skip arrays under control when it turns out that skipping isn't helpful.
A later commit that'll lower the overhead of maintaining skip arrays
will help with this by improving performance in the worst case.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |   3 +-
 src/include/access/nbtree.h                   |  35 +-
 src/include/catalog/pg_amproc.dat             |  22 +
 src/include/catalog/pg_proc.dat               |  27 +
 src/include/storage/lwlock.h                  |   1 +
 src/include/utils/skipsupport.h               |  98 +++
 src/backend/access/index/indexam.c            |   3 +-
 src/backend/access/nbtree/nbtcompare.c        | 273 ++++++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 833 ++++++++++++++++--
 src/backend/access/nbtree/nbtree.c            | 211 ++++-
 src/backend/access/nbtree/nbtsearch.c         | 104 ++-
 src/backend/access/nbtree/nbtutils.c          | 804 +++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |   4 +
 src/backend/commands/opclasscmds.c            |  25 +
 src/backend/storage/lmgr/lwlock.c             |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/date.c                  |  46 +
 src/backend/utils/adt/meson.build             |   1 +
 src/backend/utils/adt/selfuncs.c              | 394 ++++++---
 src/backend/utils/adt/skipsupport.c           |  60 ++
 src/backend/utils/adt/timestamp.c             |  48 +
 src/backend/utils/adt/uuid.c                  |  70 ++
 src/backend/utils/misc/guc_tables.c           |  23 +
 doc/src/sgml/btree.sgml                       |  34 +-
 doc/src/sgml/indexam.sgml                     |   3 +-
 doc/src/sgml/indices.sgml                     |  49 +-
 doc/src/sgml/xindex.sgml                      |  16 +-
 src/test/regress/expected/alter_generic.out   |  10 +-
 src/test/regress/expected/create_index.out    |   4 +-
 src/test/regress/expected/join.out            |  30 +-
 src/test/regress/expected/psql.out            |   3 +-
 src/test/regress/expected/union.out           |  15 +-
 src/test/regress/sql/alter_generic.sql        |   5 +-
 src/test/regress/sql/create_index.sql         |   4 +-
 src/tools/pgindent/typedefs.list              |   3 +
 36 files changed, 2927 insertions(+), 337 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index fb94b3d1a..5046b31f3 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -206,7 +206,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 6a501537e..4aafc2bc2 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -707,6 +708,10 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
  *	(BTOPTIONS_PROC).  These procedures define a set of user-visible
  *	parameters that can be used to control operator class behavior.  None of
  *	the built-in B-Tree operator classes currently register an "options" proc.
+ *
+ *	To facilitate more efficient B-Tree skip scans, an operator class may
+ *	choose to offer a sixth amproc procedure (BTSKIPSUPPORT_PROC).  For full
+ *	details, see src/include/utils/skipsupport.h.
  */
 
 #define BTORDER_PROC		1
@@ -714,7 +719,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1027,10 +1033,24 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	int16		attlen;			/* attr len in bytes */
+	bool		attbyval;		/* as FormData_pg_attribute.attbyval */
+	SkipSupportData sksup;		/* skip scan support (unless !use_sksup) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
+	FmgrInfo   *low_order;		/* low_compare's ORDER proc */
+	FmgrInfo   *high_order;		/* high_compare's ORDER proc */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1042,6 +1062,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Last array advancement matched -inf attr? */
 	bool		oppositeDirCheck;	/* explicit scanBehind recheck needed? */
@@ -1118,6 +1139,10 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on omitted prefix column */
+#define SK_BT_MINMAXVAL	0x00080000	/* lowest/highest key in array's range */
+#define SK_BT_NEXT		0x00100000	/* positions scan > sk_argument */
+#define SK_BT_PRIOR		0x00200000	/* positions scan < sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1154,6 +1179,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
@@ -1164,7 +1193,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index b334a5fb8..fcf041d23 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b37e8a6f8..52ef0db1f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2260,6 +2275,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4447,6 +4465,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6303,6 +6324,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9345,6 +9369,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 2aa46fd50..6803d5b9e 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..bc7bcfb6d
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining pairs of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code falls back on next-key sentinel values for any opclass that
+ * doesn't provide its own skip support function.  There is no benefit to
+ * providing skip support for an opclass on a type where guessing that the
+ * next indexed value is the next possible indexable value seldom works out.
+ * It might not even be feasible to add skip support for a continuous type.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order)
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 *
+	 * Operator class decrement/increment functions never have to directly
+	 * deal with the value NULL, nor with ASC vs. DESC column ordering.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern bool PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+										  bool reverse, SkipSupport sksup);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 8b1f55543..0e62d7a7a 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 291cb8fc1..4da5a3c1d 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index b026fedae..a0d0c59fd 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -21,6 +21,27 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 typedef struct BTScanKeyPreproc
 {
 	ScanKey		inkey;
@@ -35,6 +56,13 @@ typedef struct BTSortArrayContext
 	bool		reverse;
 } BTSortArrayContext;
 
+typedef struct BTSkipPreproc
+{
+	SkipSupportData sksup;		/* opclass skip scan support (optional) */
+	bool		use_sksup;		/* sksup initialized/skip support in use? */
+	Oid			eq_op;			/* = op to be used to add array, if any */
+} BTSkipPreproc;
+
 static bool _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption);
 static void _bt_mark_scankey_required(ScanKey skey);
 static bool _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
@@ -45,8 +73,17 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
+static void _bt_array_shrink(ScanKey arraysk, ScanKey skey,
+							 FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+							 bool *qual_ok);
+static bool _bt_skip_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							FmgrInfo *orderprocp, BTArrayKeyInfo *array,
+							bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_count_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+								 int *numSkipArrayKeys);
+static bool _bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt);
 static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
 									  Oid elemtype, StrategyNumber strat,
 									  Datum *elems, int nelems);
@@ -60,6 +97,12 @@ static bool _bt_merge_arrays(IndexScanDesc scan, ScanKey skey,
 							 Datum *elems_orig, int *nelems_orig,
 							 Datum *elems_next, int nelems_next);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
+static void _bt_skip_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+								  BTArrayKeyInfo *array);
+static void _bt_skip_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+									 BTArrayKeyInfo *array);
+static void _bt_skip_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+									 BTArrayKeyInfo *array);
 
 
 /*
@@ -89,6 +132,8 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -101,10 +146,16 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never generated skip array scan keys, it would be possible for "gaps"
+ * to appear that make it unsafe to mark any subsequent input scan keys
+ * (copied from scan->keyData[]) as required to continue the scan.  Prior to
+ * Postgres 18, a qual like "WHERE y = 4" always required a full index scan.
+ * It'll now become "WHERE x = ANY('{every possible x value}') and y = 4" on
+ * output, due to the addition of a skip array on "x".  This has the potential
+ * to be much more efficient than a full index scan (though it'll behave like
+ * a full index scan when there are many distinct values for "x").
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -141,7 +192,12 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -200,6 +256,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProcs[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -231,6 +295,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
+		Assert(!so->skipScan);
 
 		return;
 	}
@@ -288,7 +353,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -345,7 +411,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -393,6 +458,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, _bt_preprocess_array_keys has already added "=" keys
+			 * sufficient to form an unbroken series of "=" constraints on all
+			 * attrs prior to the attr from the final scan->keyData[] key.
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -481,6 +551,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -489,6 +560,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -508,8 +580,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -803,6 +873,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -812,6 +885,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -885,6 +974,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -991,26 +1081,28 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
  * guaranteeing that at least the scalar scan key can be considered redundant.
  * We return false if the comparison could not be made (caller must keep both
  * scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar inequality scan keys.
  */
 static bool
 _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
 							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 							   bool *qual_ok)
 {
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
-	int			cmpresult = 0,
-				cmpexact = 0,
-				matchelem,
-				new_nelems = 0;
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
+	MemoryContext oldContext;
+	bool		eliminated;
 
 	Assert(arraysk->sk_attno == skey->sk_attno);
-	Assert(array->num_elems > 0);
 	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
 		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
 	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
 	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
 		   skey->sk_strategy != BTEqualStrategyNumber);
@@ -1020,8 +1112,7 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	 * datum of opclass input type for the index's attribute (on-disk type).
 	 * We can reuse the array's ORDER proc whenever the non-array scan key's
 	 * type is a match for the corresponding attribute's input opclass type.
-	 * Otherwise, we have to do another ORDER proc lookup so that our call to
-	 * _bt_binsrch_array_skey applies the correct comparator.
+	 * Otherwise, we have to do another ORDER proc lookup.
 	 *
 	 * Note: we have to support the convention that sk_subtype == InvalidOid
 	 * means the opclass input type; this is a hack to simplify life for
@@ -1052,11 +1143,65 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 			return false;
 		}
 
-		/* We have all we need to determine redundancy/contradictoriness */
+		/* We successfully looked up the required cross-type ORDER proc */
 		orderprocp = &crosstypeproc;
 		fmgr_info(cmp_proc, orderprocp);
 	}
 
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	/*
+	 * Perform preprocessing of the array based on whether it's a conventional
+	 * array, or a skip array.  Sets *qual_ok correctly in passing.
+	 */
+	if (array->num_elems != -1)
+	{
+		/*
+		 * We successfully looked up the required cross-type ORDER proc, which
+		 * ensures that the scalar scan key can be eliminated as redundant
+		 */
+		eliminated = true;
+
+		_bt_array_shrink(arraysk, skey, orderprocp, array, qual_ok);
+	}
+	else
+	{
+		/*
+		 * With a skip array, it's possible that we won't be able to eliminate
+		 * the scalar scan key, despite looking up the required ORDER proc.
+		 * This happens when there's no cross-type support for comparing skey
+		 * to an existing inequality that's already associated with the array.
+		 */
+		eliminated = _bt_skip_shrink(scan, arraysk, skey, orderprocp, array,
+									 qual_ok);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return eliminated;
+}
+
+/*
+ * Finish off preprocessing of conventional (non-skip) array scan key when
+ * determining its redundancy against a non-array scalar scan key.
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Rewrites caller's array in-place as needed to eliminate redundant array
+ * elements.  Calling here always renders caller's scalar scan key redundant.
+ */
+static void
+_bt_array_shrink(ScanKey arraysk, ScanKey skey, FmgrInfo *orderprocp,
+				 BTArrayKeyInfo *array, bool *qual_ok)
+{
+	int			cmpresult = 0,
+				cmpexact = 0,
+				matchelem,
+				new_nelems = 0;
+
+	Assert(array->num_elems > 0);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
+
 	matchelem = _bt_binsrch_array_skey(orderprocp, false,
 									   NoMovementScanDirection,
 									   skey->sk_argument, false, array,
@@ -1108,6 +1253,124 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 
 	array->num_elems = new_nelems;
 	*qual_ok = new_nelems > 0;
+}
+
+/*
+ * Finish off preprocessing of a skip array scan key when determining its
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function, called after the relevant
+ * (potentially cross-type) ORDER proc has been looked up successfully.
+ *
+ * Unlike _bt_array_shrink, we cannot really modify caller's array in-place.
+ * Skip arrays work by procedurally generating their elements as needed, so
+ * our approach is to store a copy of the inequality in the skip array.  This
+ * forces the array's elements to be generated within the limits of a range
+ * that's described/constrained by the array's inequalities.
+ *
+ * Return value indicates if the scalar scan key could be "eliminated".  We
+ * return true in the common case where caller's scan key was successfully
+ * rolled into the skip array.  We return false when we can't do that due to
+ * the presence of an existing, conflicting inequality that cannot be compared
+ * to caller's inequality due to a lack of suitable cross-type support.
+ */
+static bool
+_bt_skip_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+				FmgrInfo *orderprocp, BTArrayKeyInfo *array, bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Store a copy of caller's scalar scan key, plus a copy of the operator's
+	 * corresponding 3-way ORDER proc.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way scan code can generally assume that
+	 * it'll always be safe to use the ORDER procs stored in so->orderProcs[].
+	 * The only exception is cases where the array's scan key is MINMAXVAL.
+	 *
+	 * When the array's scan key is marked MINMAXVAL, then it'll lack a valid
+	 * sk_argument.  The scan must apply the array's low_compare and/or
+	 * high_compare (if any) to establish whether a given tupdatum is within
+	 * the range of the skip array.  This could involve applying the 3-way
+	 * low_order/high_order ORDER proc we store here (though never the ORDER
+	 * proc stored in so->orderProcs[]).
+	 *
+	 * Note: we sometimes use low_compare/high_compare inequalities with the
+	 * array's current element value, and with values that were mutated by the
+	 * array's skip support routine.  A skip array must always use its index
+	 * attribute's own input opclass/type, so this will always work correctly.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (!array->high_compare)
+			{
+				/* array currently lacks a high_compare, make space for one */
+				array->high_compare = palloc(sizeof(ScanKeyData));
+				array->high_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new high_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new high_compare, keep old one */
+
+				/* replace old high_compare with caller's new one */
+			}
+
+			/* caller's high_compare becomes skip array's high_compare */
+			*array->high_compare = *skey;
+			*array->high_order = *orderprocp;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (!array->low_compare)
+			{
+				/* array currently lacks a low_compare, make space for one */
+				array->low_compare = palloc(sizeof(ScanKeyData));
+				array->low_order = palloc(sizeof(FmgrInfo));
+			}
+			else
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't make new low_compare redundant  */
+
+				if (!test_result)
+					return true;	/* discard new low_compare, keep old one */
+
+				/* replace old low_compare with caller's new one */
+			}
+
+			/* caller's low_compare becomes skip array's low_compare */
+			*array->low_compare = *skey;
+			*array->low_order = *orderprocp;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
 
 	return true;
 }
@@ -1137,6 +1400,12 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -1152,10 +1421,12 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	BTSkipPreproc skipatts[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
@@ -1163,30 +1434,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_count_array_keys(scan, skipatts, &numSkipArrayKeys);
+	so->skipScan = (numSkipArrayKeys > 0);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys described within skipatts[]
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -1201,17 +1466,18 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
+	/* Process input keys, and emit skip array keys described by skipatts[] */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
@@ -1227,19 +1493,101 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		int			num_nonnulls;
 		int			j;
 
+		/* Create a skip array and its scan key where indicated */
+		while (numSkipArrayKeys &&
+			   attno_skip <= scan->keyData[input_ikey].sk_attno)
+		{
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skipatts[attno_skip - 1].eq_op;
+			CompactAttribute *attr;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/* attribute already had an "=" key on input */
+				attno_skip++;
+				continue;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			cur = &arrayKeyData[numArrayKeyData];
+			Assert(attno_skip <= scan->keyData[input_ikey].sk_attno);
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			attr = TupleDescCompactAttr(RelationGetDescr(rel), attno_skip - 1);
+			/* Initialize array fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+			so->arrayKeys[numArrayKeys].cur_elem = 0;
+			so->arrayKeys[numArrayKeys].elem_values = NULL; /* unusued */
+			so->arrayKeys[numArrayKeys].use_sksup = skipatts[attno_skip - 1].use_sksup;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].attlen = attr->attlen;
+			so->arrayKeys[numArrayKeys].attbyval = attr->attbyval;
+			so->arrayKeys[numArrayKeys].sksup = skipatts[attno_skip - 1].sksup;
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].low_order = NULL;	/* for now */
+			so->arrayKeys[numArrayKeys].high_order = NULL;	/* for now */
+
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].use_sksup = false;
+
+			/*
+			 * We'll need a 3-way ORDER proc to determine when and how this
+			 * constructed "array" will advance inside _bt_advance_array_keys.
+			 * Set one up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			/*
+			 * Prepare to output next scan key (might be another skip scan
+			 * key, or it could be an input scan key from scan->keyData[])
+			 */
+			numSkipArrayKeys--;
+			numArrayKeys++;
+			attno_skip++;
+			numArrayKeyData++;	/* keep this scan key/array */
+		}
+
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
+		cur = &arrayKeyData[numArrayKeyData];
 		*cur = scan->keyData[input_ikey];
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -1295,7 +1643,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -1306,7 +1654,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -1323,7 +1671,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -1398,17 +1746,25 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
 		 * scan_key field later on, after so->keyData[] has been finalized.
 		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].null_elem = false;	/* unused */
+		so->arrayKeys[numArrayKeys].attlen = 0; /* unused */
+		so->arrayKeys[numArrayKeys].attbyval = false;	/* unused */
+		so->arrayKeys[numArrayKeys].use_sksup = false;	/* redundant */
+		so->arrayKeys[numArrayKeys].low_compare = NULL; /* unused */
+		so->arrayKeys[numArrayKeys].high_compare = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].low_order = NULL;	/* unused */
+		so->arrayKeys[numArrayKeys].high_order = NULL;	/* unused */
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -1514,7 +1870,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -1550,6 +1907,15 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && !array->null_elem &&
+						array->use_sksup)
+						_bt_skip_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
@@ -1565,7 +1931,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -1575,6 +1941,209 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_count_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit every so->arrayKeys[] entry.
+ *
+ * Also sets *numSkipArrayKeys to the number of skip arrays that caller must
+ * make sure to add to the scan keys it'll output (complete with corresponding
+ * so->arrayKeys[] entries), and indicates which specific index columns are to
+ * receive skip arrays using caller's *skipatts argument.  Caller must add a
+ * skip array to attributes whose BTSkipPreproc.eq_op was set to a valid oid.
+ */
+static int
+_bt_count_array_keys(IndexScanDesc scan, BTSkipPreproc *skipatts,
+					 int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_inkey = 1,
+				attno_skip = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+	/*
+	 * Only add skip arrays (and associated scan keys) when doing so will
+	 * enable _bt_preprocess_keys to mark one or more lower-order input scan
+	 * keys (user-visible scan keys taken from scan->keyData[] input array) as
+	 * required to continue the scan
+	 */
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			if (!_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key.
+		 *
+		 * If the last input scan key(s) use equality strategy, then a skip
+		 * attribute is superfluous at best.  If the last input scan key uses
+		 * an inequality strategy, then adding a skip scan array/scan key is a
+		 * valid though suboptimal transformation.  It is better to arrange
+		 * for preprocessing to allow such an input inequality scan key to
+		 * remain an inequality on output.  That way _bt_checkkeys will be
+		 * able to make use of its 'firstmatch' optimization.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys.
+			 *
+			 * Adding skip arrays to an attribute that has one or more
+			 * inequality scan keys will cause preprocessing to output a range
+			 * skip array.  This will happen when later preprocessing deals
+			 * with the redundancy between the array and its inequalities.
+			 */
+			skipatts[attno_skip - 1].eq_op = InvalidOid;
+			if (attno_has_equal)
+			{
+				/* Don't skip, attribute already has an input equality key */
+			}
+			else if (_bt_skipsupport(rel, attno_skip, &skipatts[attno_skip - 1]))
+			{
+				/*
+				 * Saw no equalities for the prior attribute, so add a range
+				 * skip array for this attribute
+				 */
+				(*numSkipArrayKeys)++;
+			}
+			else
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				break;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required later on.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
+/*
+ *	_bt_skipsupport() -- set up skip support function in *skipatt
+ *
+ * Returns true on success, indicating that we set *skipatts with input
+ * opclass's equality operator.  Otherwise returns false (only possible for an
+ * index attribute whose input opclass comes from an incomplete opfamily).
+ */
+static bool
+_bt_skipsupport(Relation rel, int add_skip_attno, BTSkipPreproc *skipatt)
+{
+	int16	   *indoption = rel->rd_indoption;
+	Oid			opfamily = rel->rd_opfamily[add_skip_attno - 1];
+	Oid			opcintype = rel->rd_opcintype[add_skip_attno - 1];
+	bool		reverse;
+
+	/* Look up input opclass's equality operator (might fail) */
+	skipatt->eq_op = get_opfamily_member(opfamily, opcintype, opcintype,
+										 BTEqualStrategyNumber);
+
+	/*
+	 * We don't really expect opclasses that lack even an equality strategy
+	 * operator, but they're still supported.  Cope by making caller not use a
+	 * skip array for this index column (nor for any lower-order columns).
+	 */
+	if (!OidIsValid(skipatt->eq_op))
+		return false;
+
+	/* Have skip support infrastructure set all SkipSupport fields */
+	reverse = (indoption[add_skip_attno - 1] & INDOPTION_DESC) != 0;
+	skipatt->use_sksup = PrepareSkipSupportFromOpclass(opfamily, opcintype,
+													   reverse,
+													   &skipatt->sksup);
+
+	/* might not have set up skip support, but can skip either way */
+	return true;
+}
+
 /*
  * _bt_find_extreme_element() -- get least or greatest array element
  *
@@ -1862,3 +2431,161 @@ _bt_compare_array_elements(const void *a, const void *b, void *arg)
 		INVERT_COMPARE_RESULT(compare);
 	return compare;
 }
+
+/*
+ * Perform final preprocessing for a skip array.  Called when the skip array's
+ * final low_compare and high_compare have been chosen.
+ *
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts the
+ * operator strategies).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value MINMAXVAL, using >= instead of using >
+ * (or using <= instead of < during backwards scans) makes it safe to include
+ * lower-order scan keys in the insertion scan key (there must be lower-order
+ * scan keys after the skip array).  We will avoid an extra _bt_first to find
+ * the first value in the index > sk_argument, at least when the first real
+ * matching value in the index happens to be an exact match for the newly
+ * transformed (newly incremented/decremented) sk_argument value.
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01' -- which would be wrong.
+ */
+static void
+_bt_skip_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+					  BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1 && !array->null_elem && array->use_sksup);
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skip_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skip_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skip_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+						 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+	Assert(array->use_sksup);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup.decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skip_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+						 BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+	Assert(array->use_sksup);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup.increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 6345a37a8..38d848994 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -29,6 +29,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -70,7 +71,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -78,11 +79,24 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 *
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -409,6 +423,7 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 		memcpy(scan->keyData, scankey, scan->numberOfKeys * sizeof(ScanKeyData));
 	so->numberOfKeys = 0;		/* until _bt_preprocess_keys sets it */
 	so->numArrayKeys = 0;		/* ditto */
+	so->skipScan = false;		/* tracks if skip arrays are in use */
 }
 
 /*
@@ -535,10 +550,155 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume all input scankeys will be output with its own
+	 * SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * scan array (and associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		CompactAttribute *attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescCompactAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array/skip scan key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_MINMAXVAL)
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   array->attbyval, array->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array/skip scan key, while freeing memory allocated
+		 * for old sk_argument where required
+		 */
+		if (!array->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_MINMAXVAL)
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -549,7 +709,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -572,16 +733,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -608,6 +769,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -652,7 +814,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -674,14 +836,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -719,7 +876,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -763,11 +920,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -806,7 +963,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -815,7 +972,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -833,6 +990,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -842,7 +1000,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -852,14 +1010,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 941b4eaaf..fef84db86 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -964,6 +964,15 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  All
+	 * array keys are always considered = keys, but we'll sometimes need to
+	 * treat the current key value as if we were using an inequality strategy.
+	 * This happens whenever a skip array's scan key is found to have been set
+	 * to one of the special sentinel values representing an imaginary point
+	 * before/after some (or all) of the index's real indexable values.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1039,8 +1048,46 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is MINMAXVAL, just choose low_compare/high_compare
+				 * (or consider if they imply a NOT NULL constraint).
+				 *
+				 * Note: if the array's low_compare key makes 'chosen' NULL,
+				 * then we behave as if the array's first element is -inf,
+				 * unless high_compare implies a usable NOT NULL constraint.
+				 * (It works the other way around during backwards scans.)
+				 */
+				if (chosen && (chosen->sk_flags & SK_BT_MINMAXVAL))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skiparraysk = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					Assert(so->skipScan);
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					if (!array->null_elem)
+						impliesNN = skiparraysk;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1083,9 +1130,44 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 
 				/*
-				 * Done if that was the last attribute, or if next key is not
-				 * in sequence (implying no boundary key is available for the
-				 * next attribute).
+				 * If this is a scan key for a skip array whose current
+				 * element is marked NEXT or PRIOR, adjust strat_total
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(so->skipScan);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(strat_total == BTEqualStrategyNumber);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[].
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).
+					 */
+					break;
+				}
+
+				/*
+				 * Done if that was the last index attribute.  Also done if
+				 * there is a gap index attribute that lacks any usable keys
+				 * (only possible in edge cases where preprocessing could not
+				 * generate a skip array key that "fills the gap").
 				 */
 				if (i >= so->numberOfKeys ||
 					cur->sk_attno != curattr + 1)
@@ -1576,10 +1658,12 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * corresponding value from the last item on the page.  So checking with
 	 * the last item on the page would give a more precise answer.
 	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
+	 * We don't do this for the first page read by each (primitive) scan, to
+	 * avoid slowing down point queries.  They typically don't stand to gain
+	 * much when the optimization can be applied, and are more likely to
+	 * notice the overhead of the precheck.  Also avoid it during skip scans,
+	 * where individual primitive scans that don't just read one leaf page are
+	 * likely to advance their skip array(s) several times per leaf page read.
 	 *
 	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
 	 * just set a low-order required array's key to the best available match
@@ -1603,7 +1687,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!firstPage && !so->scanBehind && minoff < maxoff)
+	if (!firstPage && !so->skipScan && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 693e43c67..e1632673c 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -30,6 +30,19 @@
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_scankey_set_low_or_high(Relation rel, ScanKey skey,
+										BTArrayKeyInfo *array, bool low_not_high);
+static void _bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									Datum tupdatum, bool tupnull);
+static void _bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -205,6 +218,7 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 	int32		result = 0;
 
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
+	Assert(!(cur->sk_flags & SK_BT_MINMAXVAL));
 
 	if (tupnull)				/* NULL tupdatum */
 	{
@@ -281,6 +295,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -403,6 +419,99 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, because the array doesn't really
+ * have any elements (it generates its array elements procedurally instead).
+ * Note that non-range skip arrays have an element for the value NULL, which
+ * is applied as an IS NULL qual within _bt_checkkeys.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ *
+ * Note: This function doesn't actually binary search.  It uses an interface
+ * that's as similar to _bt_binsrch_array_skey as possible for consistency.
+ * "Binary searching a skip array" just means determining if tupdatum/tupnull
+ * are before, within, or beyond the range of caller's skip array.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -412,29 +521,457 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_scankey_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_scankey_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+							bool low_not_high)
+{
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else
+	{
+		/* Setting skip array to lowest/highest non-NULL element */
+		skey->sk_flags |= SK_BT_MINMAXVAL;
+	}
+}
+
+/*
+ * _bt_scankey_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key to "IS NULL" when required, and handles memory management for
+ * pass-by-reference types.
+ */
+static void
+_bt_scankey_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						Datum tupdatum, bool tupnull)
+{
+	/* tupdatum within the range of low_value/high_value */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(tupnull && !array->null_elem));
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR);
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_unset_isnull() -- increment/decrement scan key from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_scankey_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->use_sksup && array->null_elem &&
+		   !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup.low_elem,
+									  array->attbyval, array->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup.high_elem,
+									  array->attbyval, array->attlen);
+}
+
+/*
+ * _bt_scankey_set_isnull() -- decrement/increment scan key to NULL
+ */
+static void
+_bt_scankey_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_scankey_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_scankey_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_MINMAXVAL));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the minimum value within the range
+	 * of a skip array (often just -inf) is never decrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINMAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the prior element is outside of the range of the array.
+		 *
+		 * This is only possible if low_compare represents an >= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the prior value cannot possibly satisfy low_compare, so we can
+		 * give up right away.
+		 */
+		if (array->low_compare &&
+			array->low_compare->sk_strategy == BTGreaterEqualStrategyNumber &&
+			_bt_compare_array_skey(array->low_order,
+								   array->low_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Decrement by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup.decrement(rel, skey->sk_argument, &uflow);
+	if (unlikely(uflow))
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_scankey_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_scankey_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_MINMAXVAL));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the maximum value within the range
+	 * of a skip array (often just +inf) is never incrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINMAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->use_sksup)
+	{
+		/*
+		 * Determine as best we can (given the lack of skip support) whether
+		 * the next element is outside of the range of the array.
+		 *
+		 * This is only possible if high_compare represents an <= inequality.
+		 * If the current array element is == the inequality's sk_argument,
+		 * then the next value cannot possibly satisfy high_compare, so we can
+		 * give up right away.
+		 */
+		if (array->high_compare &&
+			array->high_compare->sk_strategy == BTLessEqualStrategyNumber &&
+			_bt_compare_array_skey(array->high_order,
+								   array->high_compare->sk_argument, false,
+								   skey->sk_argument, skey) == 0)
+			return false;
+
+		/* Increment by setting flag for existing sk_argument/array element */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_scankey_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup.increment(rel, skey->sk_argument, &oflow);
+	if (unlikely(oflow))
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_scankey_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -450,6 +987,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -459,29 +997,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_scankey_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_scankey_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_scankey_set_low_or_high(rel, skey, array,
+									ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -541,6 +1080,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -548,7 +1088,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -560,16 +1099,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_scankey_set_low_or_high(rel, cur, array,
+									ScanDirectionIsForward(dir));
 	}
 }
 
@@ -694,9 +1227,73 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_MINMAXVAL))
+		{
+			/* Scankey has a valid/comparable sk_argument value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0)
+			{
+				/*
+				 * Interpret result in a way that takes NEXT/PRIOR into
+				 * account
+				 */
+				if (cur->sk_flags & SK_BT_NEXT)
+					result = -1;
+				else if (cur->sk_flags & SK_BT_PRIOR)
+					result = 1;
+
+				Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP));
+			}
+		}
+		else
+		{
+			/*
+			 * Current array element/array = scan key value is a sentinel
+			 * value that represents the lowest (or highest) possible value
+			 * that's still within the range of the array.
+			 *
+			 * We don't have a valid sk_argument value from = scan key.  Check
+			 * if tupdatum is within the range of skip array instead.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum satisfies both low_compare and high_compare, so
+				 * it's time to advance the array keys.
+				 *
+				 * Note: It's possible that the skip array will "advance" from
+				 * its current MINMAXVAL representation to an alternative
+				 * representation where the = key gets a useful sk_argument,
+				 * even though both representations are logically equivalent.
+				 * This is possible when low_compare uses the <= strategy, and
+				 * when high_compare uses the >= strategy.  Allowing two
+				 * distinct representations of the same array value keeps
+				 * things simple in scenarios involving cross-type operators.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1032,18 +1629,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1068,18 +1656,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_scankey_set_low_or_high(rel, cur, array,
+											ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -1097,12 +1676,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -1178,11 +1766,63 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		if (!array)
+			continue;			/* cannot advance a non-array */
+
+		/* Advance array keys, even when we don't have an exact match */
+		if (array->num_elems != -1)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			/* Conventional array, use set_elem... */
+			if (array->cur_elem != set_elem)
+			{
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
+
+			continue;
+		}
+
+		/*
+		 * ...or skip array, which doesn't advance using a set_elem offset.
+		 *
+		 * Array "contains" elements for every possible datum from a given
+		 * range of values.  "Binary searching" only determined whether
+		 * tupdatum is beyond, before, or within the range of the skip array.
+		 *
+		 * As always, we set the array element to its closest available match.
+		 * But unlike with a conventional array, a skip array's new element
+		 * might be MINMAXVAL, which represents the lowest (or the highest)
+		 * real value that's still within the range of its skip array.
+		 */
+		Assert(so->skipScan && required);
+		if (beyond_end_advance)
+		{
+			/*
+			 * tupdatum/tupnull is > the skip array's "final element"
+			 * (tupdatum/tupnull is < the "first element" for backwards scans)
+			 */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsBackward(dir));
+		}
+		else if (!all_required_satisfied)
+		{
+			/*
+			 * tupdatum/tupnull is < the skip array's "first element"
+			 * (tupdatum/tupnull is > the "final element" for backwards scans)
+			 */
+			Assert(sktrig < ikey);	/* check on _bt_tuple_before_array_skeys */
+			_bt_scankey_set_low_or_high(rel, cur, array,
+										ScanDirectionIsForward(dir));
+		}
+		else
+		{
+			/*
+			 * tupdatum/tupnull is == "some array element".
+			 *
+			 * Set scan key's sk_argument to tupdatum.  If tupdatum is null,
+			 * we'll set IS NULL flags in scan key's sk_flags instead.
+			 */
+			_bt_scankey_set_element(rel, cur, array, tupdatum, tupnull);
 		}
 	}
 
@@ -1575,10 +2215,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -1901,6 +2542,21 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key uses one of several sentinel values (usually
+		 * only at the start or end of each primitive index scan).  We always
+		 * fall back on _bt_tuple_before_array_skeys to decide what to do.
+		 */
+		if (key->sk_flags & (SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index b87c959a2..c2dd458c8 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 2c325badf..303622f9d 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 2f558ffea..39972dd0e 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0b53cba80..fa82328ef 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -370,6 +370,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index f279853de..4227ab1a7 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f23cfad71..244f48f4f 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 93e4a8906..2d44f0161 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -193,6 +193,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -214,6 +216,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5759,6 +5763,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6817,6 +6907,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6826,17 +6963,22 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		upperInequalHere;
+	bool		lowerInequalHere;
+	bool		have_correlation = false;
+	bool		found_skip;
 	bool		found_saop;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	double		inequalselectivity = 1.0;
 	double		num_sa_scans;
+	double		correlation = 0;
 	ListCell   *lc;
 
 	/*
@@ -6852,14 +6994,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
+	upperInequalHere = false;
+	lowerInequalHere = false;
+	found_skip = false;
 	found_saop = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -6869,13 +7016,90 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		first = true;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't absord a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						correlation = btcost_correlation(index, &vardata);
+						have_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (first)
+				{
+					first = false;
+
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is..
+					 */
+					if (!upperInequalHere)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lowerInequalHere)
+						ndistinct += 1;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				num_sa_scans *= ndistinct;
+
+				/* Done costing skipping for this index column */
+				indexcol++;
+				found_skip = true;
+			}
+
+			/* new index column resets tracking variables */
 			eqQualHere = false;
-			indexcol++;
-			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+			upperInequalHere = false;
+			lowerInequalHere = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6897,6 +7121,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6917,7 +7142,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skipping purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6933,6 +7158,38 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					bool		useinequal = true;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (upperInequalHere)
+							useinequal = false;
+						upperInequalHere = true;
+					}
+					else
+					{
+						if (lowerInequalHere)
+							useinequal = false;
+						lowerInequalHere = true;
+					}
+
+					/*
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (useinequal)
+						inequalselectivity =
+							Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+								DEFAULT_RANGE_INEQ_SEL);
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6948,6 +7205,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
+		!found_skip &&
 		!found_saop &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
@@ -6976,7 +7234,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * natural ceiling on the worst case number of descents -- there
 		 * cannot possibly be more than one descent per leaf page scanned.
 		 *
-		 * Clamp the number of descents to at most 1/3 the number of index
+		 * Clamp the number of descents to at most the total number of index
 		 * pages.  This avoids implausibly high estimates with low selectivity
 		 * paths, where scans usually require only one or two descents.  This
 		 * is most likely to help when there are several SAOP clauses, where
@@ -6984,18 +7242,17 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * array elements as the number of descents would frequently lead to
 		 * wild overestimates.
 		 *
-		 * We somewhat arbitrarily don't just make the cutoff the total number
-		 * of leaf pages (we make it 1/3 the total number of pages instead) to
-		 * give the btree code credit for its ability to continue on the leaf
-		 * level with low selectivity scans.
+		 * Note: num_sa_scans is derived in a way that treats any skip scan
+		 * quals as if they were ScalarArrayOpExpr quals whose array has as
+		 * many distinct elements as possibly-matching values in the index.
 		 */
-		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
+		num_sa_scans = Min(num_sa_scans, index->pages);
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
 		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
-		 * to integer.
+		 * ScalarArrayOpExpr quals (as well as any skip scan quals) included
+		 * in indexBoundQuals, and then round to integer.
 		 *
 		 * It is tempting to make genericcostestimate behave as if SAOP
 		 * clauses work in almost the same way as scalar operators during
@@ -7056,104 +7313,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..90ddfd314
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,60 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns true, and initializes all SkipSupport fields for
+ * caller.  Otherwise returns false, indicating that operator class has no
+ * skip support function.
+ */
+bool
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse,
+							  SkipSupport sksup)
+{
+	Oid			skipSupportFunction;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return false;
+
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return true;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index ba9bae050..f64fbf263 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2286,6 +2287,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 4f8402ef9..1b72059d2 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,6 +13,7 @@
 
 #include "postgres.h"
 
+#include <limits.h>
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
@@ -21,6 +22,7 @@
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -416,6 +418,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index c9d8cd796..8161ab7ee 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1761,6 +1762,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3618,6 +3630,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15..ee26dbecc 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 8011c141b..1dde8110c 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2248,7 +2248,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2268,7 +2268,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 079fcf46f..ac029e641 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4527,24 +4527,25 @@ select b.unique1 from
   join int4_tbl i1 on b.thousand = f1
   right join int4_tbl i2 on i2.f1 = b.tenthous
   order by 1;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: b.unique1
    ->  Nested Loop Left Join
          ->  Seq Scan on int4_tbl i2
-         ->  Nested Loop Left Join
-               Join Filter: (b.unique1 = 42)
-               ->  Nested Loop
+         ->  Nested Loop
+               Join Filter: (b.thousand = i1.f1)
+               ->  Nested Loop Left Join
+                     Join Filter: (b.unique1 = 42)
                      ->  Nested Loop
-                           ->  Seq Scan on int4_tbl i1
                            ->  Index Scan using tenk1_thous_tenthous on tenk1 b
-                                 Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1))
-                     ->  Index Scan using tenk1_unique1 on tenk1 a
-                           Index Cond: (unique1 = b.unique2)
-               ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
-                     Index Cond: (thousand = a.thousand)
-(15 rows)
+                                 Index Cond: (tenthous = i2.f1)
+                           ->  Index Scan using tenk1_unique1 on tenk1 a
+                                 Index Cond: (unique1 = b.unique2)
+                     ->  Index Only Scan using tenk1_thous_tenthous on tenk1 c
+                           Index Cond: (thousand = a.thousand)
+               ->  Seq Scan on int4_tbl i1
+(16 rows)
 
 select b.unique1 from
   tenk1 a join tenk1 b on a.unique1 = b.unique2
@@ -8070,9 +8071,10 @@ where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1 and j2.id1 >= any (array[1,5]);
    Merge Cond: (j1.id1 = j2.id1)
    Join Filter: (j2.id2 = j1.id2)
    ->  Index Scan using j1_id1_idx on j1
-   ->  Index Scan using j2_id1_idx on j2
+   ->  Index Only Scan using j2_pkey on j2
          Index Cond: (id1 >= ANY ('{1,5}'::integer[]))
-(6 rows)
+         Filter: ((id1 % 1000) = 1)
+(7 rows)
 
 select * from j1
 inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 36dc31c16..aef239200 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5240,9 +5240,10 @@ List of access methods
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index caa8fe70a..96962817e 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1472,18 +1472,17 @@ select t1.unique1 from tenk1 t1
 inner join tenk2 t2 on t1.tenthous = t2.tenthous and t2.thousand = 0
    union all
 (values(1)) limit 1;
-                       QUERY PLAN                       
---------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Limit
    ->  Append
          ->  Nested Loop
-               Join Filter: (t1.tenthous = t2.tenthous)
-               ->  Seq Scan on tenk1 t1
-               ->  Materialize
-                     ->  Seq Scan on tenk2 t2
-                           Filter: (thousand = 0)
+               ->  Seq Scan on tenk2 t2
+                     Filter: (thousand = 0)
+               ->  Index Scan using tenk1_thous_tenthous on tenk1 t1
+                     Index Cond: (tenthous = t2.tenthous)
          ->  Result
-(9 rows)
+(8 rows)
 
 -- Ensure there is no problem if cheapest_startup_path is NULL
 explain (costs off)
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index 068c66b95..622e55f72 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -858,7 +858,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -868,7 +868,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index eb93debe1..5ffc247e8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -219,6 +219,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2675,6 +2676,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.47.1

In reply to: Peter Geoghegan (#58)
5 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Mon, Jan 13, 2025 at 3:22 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v21. This revision is just to fix bitrot against HEAD that
was caused by recent commits of mine -- all of which were related to
nbtree preprocessing.

Attached is v22.

This revision changes the optimizer's cost model, making its costing
exactly match the costing on the master branch in marginal cases --
cases where some skipping may be possible, but not enough to justify
*expecting* any saving during query planning.

Perhaps surprisingly, skipping only every second leaf page can be
5x-7x faster, even though the number of "buffers hit" could easily be
~3x higher due to all of the extra internal page reads. But it's
really hard to predict exactly how much we'll benefit from skipping
during planning, within btcostestimate. The costing is of course
driven by statistics, and estimating the cardinality of multiple
columns together with those statistics is bound to be quite inaccurate
much of the time. We should err in the direction of assuming a
relatively expensive full index scan (we always did, but now we do so
even more).

As a result of these improvements to the costing, v22 is the first
version without any changes to EXPLAIN output/query plans in expected
regression test output. That wasn't what I set out to do (I actually
set out to fix clearly nonsensical costing in certain edge cases), but
not having any plan changes in the regression tests does seem like a
good thing to me.

v22 also simplifies a number of things on the nbtree side:

* We no longer keep around a possibly-cross-type ORDER proc in skip
arrays (just the original scan key). We give up on a marginal
optimization that was used only during skip scans involving a skipped
column with a range containing either >= or <= inequalities for a
type/opclass that lacks skip support.

In v22, nbtree scans can no longer determine that a tuple "(a, b) =
('foo', 342)" with a qual "WHERE a <= 'foo' AND b = 342" doesn't have
to continue once it reaches the first tuple > '(foo, 342)' when "a" is
of a type that doesn't offer skip support, such as text (if "a" is of
a type like integer then we still get this behavior, without any of
the complexity). The downside of ripping this optimization out is that
there might now be an extra primitive index scan that finds the next
"a" value is > 'foo' before we can actually terminate the scan --
we'll now fail to notice that the existing skip array element is
'foo', so the next one in the index cannot possibly be greater than
'foo'. The upside is that it makes things simpler, and avoids extra
comparisons during scans that might not pay for themselves.

This optimization wasn't adding very much, and didn't seem to justify
the complexity that it imposed during preprocessing. Keeping around
extra ORDER procs had problems in cases that happened to involve
cross-type operators. I'm pretty sure that they were broken in the
presence of a redundant/duplicative inequality that couldn't be proven
to be safe to eliminate by preprocessing. I probably could have fixed
the problem instead, but it seemed better to just cut scope.

* The optimization that has nbtree preprocessing convert "WHERE a > 5
AND b = 9000" into "WHERE b >= 6 and b = 9000" where possible (i.e. in
cases involving a skip array that offers skip support) has been broken
out into its own commit/patch -- that's now in 0004-*.

It's possible that I'll ultimately conclude that this optimization
isn't worth the complexity, either -- and then rip it out as well. But
it isn't all that complicated, and only imposes overhead during
preprocessing (never during the scan proper), so I still lean towards
committing it. But it's certainly not essential.

* Reorders and renames the new functions in nbtutils.c and in
nbtpreprocesskeys.c for clarity.

* Polishes and slightly refactors array preprocessing, to make it
easier to understand the rules that determine when and how
preprocessing generates skip arrays.

--
Peter Geoghegan

Attachments:

v22-0005-DEBUG-Add-skip-scan-disable-GUCs.patchapplication/octet-stream; name=v22-0005-DEBUG-Add-skip-scan-disable-GUCs.patchDownload
From c78526e5effa4c0e4f8fcd74b60f3b9af9f4aea6 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 18 Jan 2025 10:54:44 -0500
Subject: [PATCH v22 5/5] DEBUG: Add skip scan disable GUCs.

---
 src/include/access/nbtree.h                   |  4 +++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 31 +++++++++++++++++++
 src/backend/utils/misc/guc_tables.c           | 23 ++++++++++++++
 3 files changed, 58 insertions(+)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 6dc4a04d2..609b0ead2 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1178,6 +1178,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 0838ba964..c4a50127e 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -21,6 +21,27 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 typedef struct BTScanKeyPreproc
 {
 	ScanKey		inkey;
@@ -1630,6 +1651,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
 			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
 
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].sksup = NULL;
+
 			/*
 			 * We'll need a 3-way ORDER proc to perform "binary searches" for
 			 * the next matching array element.  Set that up now.
@@ -2135,6 +2160,12 @@ _bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
 		if (attno_has_rowcompare)
 			break;
 
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
 		/*
 		 * Now consider next attno_inkey (or keep going if this is an
 		 * additional scan key against the same attribute)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 38cb9e970..eda66e64c 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1762,6 +1763,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3619,6 +3631,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
-- 
2.47.2

v22-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v22-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From 59c8fb875fbb3d09ab88ca41c30719786aeb6315 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v22 1/5] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 contrib/bloom/blscan.c                        |   1 +
 doc/src/sgml/bloom.sgml                       |   7 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 22 files changed, 244 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index dc6e01842..0d1ad8379 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -147,6 +147,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 4289142e2..f6fcf0125 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -585,6 +585,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index 7d1e66152..81440a283 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index cc40e928e..609e85fda 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index a3a1fccf3..c4f730437 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 07bae342e..369e39bc4 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 3d617f168..6345a37a8 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -69,6 +69,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -552,6 +553,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -578,6 +580,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -705,6 +708,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			Assert(btscan->btps_nextScanPage != P_NONE);
 			*next_scan_page = btscan->btps_nextScanPage;
@@ -805,6 +813,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -839,6 +849,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 472ce06f1..941b4eaaf 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -950,6 +950,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 986362a77..991f99b27 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c24e66f82..6b65037cd 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2105,6 +2107,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2118,6 +2121,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2134,6 +2138,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2652,6 +2657,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index bf801fe78..472169d61 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -116,6 +116,7 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++)
 	{
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 6a8a60b8c..4cddfaae1 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -174,9 +174,10 @@ CREATE INDEX
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..178436.00 rows=1 width=0) (actual time=20.005..20.005 rows=2300 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
          Buffers: shared hit=19608
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 22.632 ms
-(10 rows)
+(11 rows)
 </programlisting>
   </para>
 
@@ -209,12 +210,14 @@ CREATE INDEX
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..4.52 rows=11 width=0) (actual time=0.026..0.026 rows=7 loops=1)
                Index Cond: (i5 = 123451)
                Buffers: shared hit=3
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..4.52 rows=11 width=0) (actual time=0.007..0.007 rows=8 loops=1)
                Index Cond: (i2 = 898732)
                Buffers: shared hit=3
+               Index Searches: 1
  Planning Time: 0.264 ms
  Execution Time: 0.047 ms
-(13 rows)
+(15 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index e5888fae2..9e0d6b503 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4211,12 +4211,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aab..34225c010 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -730,9 +730,11 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
                Buffers: shared hit=2
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
          Buffers: shared hit=24 read=6
+         Index Searches: 1
  Planning:
    Buffers: shared hit=15 dirtied=9
  Planning Time: 0.485 ms
@@ -791,6 +793,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
                            Buffers: shared hit=2
+                           Index Searches: 1
  Planning:
    Buffers: shared hit=12
  Planning Time: 0.187 ms
@@ -860,6 +863,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
    Buffers: shared hit=1
  Planning Time: 0.039 ms
@@ -894,8 +898,10 @@ EXPLAIN (ANALYZE, BUFFERS OFF) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND un
    -&gt;  BitmapAnd  (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
  Planning Time: 0.162 ms
  Execution Time: 0.143 ms
 </screen>
@@ -924,6 +930,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
                Buffers: shared read=2
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1059,6 +1066,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
    Buffers: shared hit=16
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
          Buffers: shared hit=16
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index 6361a14e6..6fc9bfedc 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -506,9 +506,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
          Buffers: shared hit=4
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(9 rows)
+(10 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 9fdf8b1d9..80a63b5f1 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index f2d146581..a5df4107a 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 5ecf971da..d9df5c0e1 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index f0707e7f7..6136a7024 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2369,6 +2369,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2686,47 +2690,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2742,16 +2755,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2770,7 +2786,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2786,16 +2802,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2816,7 +2835,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2887,16 +2906,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2904,17 +2926,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2990,17 +3015,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -3011,17 +3042,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3056,17 +3093,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3077,17 +3120,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3141,17 +3190,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3173,17 +3228,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3511,12 +3572,14 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3532,9 +3595,10 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3582,13 +3646,17 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4158,14 +4226,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 88911ca2b..cde2eac73 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index d5aab4e56..2197b0ac5 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index ea9a4fe4a..7cddf2795 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -588,6 +588,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.47.2

v22-0003-Lower-the-overhead-of-nbtree-runtime-skip-checks.patchapplication/octet-stream; name=v22-0003-Lower-the-overhead-of-nbtree-runtime-skip-checks.patchDownload
From 1c84e887e0cb3f6cf1c7d7f4a6e78916fa8df2ab Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v22 3/5] Lower the overhead of nbtree runtime skip checks.

Add a new "skipskip" strategy to fix regressions in index scans that are
nominally eligible to use skip scan, but can never actually benefit from
skipping.  These are cases where a leading prefix column contains many
distinct values -- typically as many distinct values are there are total
index tuples.  This works by dynamically falling back on a strategy that
temporarily treats all user-supplied scan keys as if they were marked
non-required, while avoiding all skip array maintenance.  The new
optimization is applied for all pages beyond each primitive index scan's
first leaf page read.

Note that this commit doesn't actually change anything about when or how
skip scan decides to schedule new primitive index scans.  It is limited
to saving CPU cycles by varying how we read individual index tuples in
cases where maintaining skip arrays cannot possibly pay for itself.

Fixing (or significantly ameliorating) regressions in the worst case
like this enables skip scan's approach within the planner.  The planner
doesn't generate distinct index paths to represent nbtree index skip
scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  The planner considers skipping when
costing relevant index paths, but that in itself won't influence skip
scan's runtime behavior.

This approach makes skip scan adapt to skewed key distributions.  It
also makes planning less sensitive to misestimations.  An index path
with an inaccurate estimate of the total number of required primitive
index scans won't have to perform many more primitive scans at runtime
(it'll behave like a traditional full index scan instead).

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h           |   3 +
 src/backend/access/nbtree/nbtsearch.c |  33 ++++
 src/backend/access/nbtree/nbtutils.c  | 264 ++++++++++++++++++++++----
 3 files changed, 261 insertions(+), 39 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index d5e01692c..6dc4a04d2 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1119,6 +1119,8 @@ typedef struct BTReadPageState
 	 */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
+	bool		skipskip;		/* skip maintenance of skip arrays? */
+	int			ikey;			/* Start comparisons from ikey'th scan key */
 
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
@@ -1319,6 +1321,7 @@ extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arra
 						  IndexTuple tuple, int tupnatts);
 extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 								  IndexTuple finaltup);
+extern void _bt_checkkeys_skipskip(IndexScanDesc scan, BTReadPageState *pstate);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 947608945..0e23edab6 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1645,6 +1645,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.continuescan = true; /* default assumption */
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
+	pstate.skipskip = false;
+	pstate.ikey = 0;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
@@ -1704,6 +1706,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 		pstate.continuescan = true; /* reset */
 	}
 
+	/*
+	 * Skip maintenance of skip arrays (if any) during primitive index scans
+	 * that read leaf pages after the first
+	 */
+	else if (!firstPage && so->skipScan && minoff < maxoff)
+		_bt_checkkeys_skipskip(scan, &pstate);
+
 	if (ScanDirectionIsForward(dir))
 	{
 		/* SK_SEARCHARRAY forward scans must provide high key up front */
@@ -1775,6 +1784,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum < pstate.skip);
+				Assert(!pstate.skipskip);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1840,6 +1850,17 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
+			if (pstate.skipskip)
+			{
+				/*
+				 * reset array keys for finaltup call, since skipskip
+				 * optimization prevented ordinary array maintenance
+				 */
+				Assert(so->skipScan);
+				_bt_start_array_keys(scan, dir);
+				pstate.skipskip = false;
+				pstate.ikey = 0;
+			}
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -1900,6 +1921,17 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff && pstate.skipskip)
+			{
+				/*
+				 * reset array keys for finaltup call, since skipskip
+				 * optimization prevented ordinary array maintenance
+				 */
+				Assert(so->skipScan);
+				_bt_start_array_keys(scan, dir);
+				pstate.skipskip = false;
+				pstate.ikey = 0;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
@@ -1911,6 +1943,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum > pstate.skip);
+				Assert(!pstate.skipskip);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index dd0f18234..d78ebf054 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -57,11 +57,12 @@ static bool _bt_verify_keys_with_arraykeys(IndexScanDesc scan);
 #endif
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool skipskip,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-								 ScanDirection dir, bool *continuescan);
+								 ScanDirection dir, bool skipskip, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -1405,14 +1406,14 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
  * postcondition's <= operator with a >=.  In other words, just swap the
  * precondition with the postcondition.)
  *
- * We also deal with "advancing" non-required arrays here.  Callers whose
- * sktrig scan key is non-required specify sktrig_required=false.  These calls
- * are the only exception to the general rule about always advancing the
- * required array keys (the scan may not even have a required array).  These
- * callers should just pass a NULL pstate (since there is never any question
- * of stopping the scan).  No call to _bt_tuple_before_array_skeys is required
- * ahead of these calls (it's already clear that any required scan keys must
- * be satisfied by caller's tuple).
+ * We also deal with "advancing" non-required arrays here (or arrays that are
+ * temporarily being treated as non-required due to the application of the
+ * "skipskip" optimization).  Callers whose sktrig scan key is non-required
+ * specify sktrig_required=false.  These calls are the only exception to the
+ * general rule about always advancing the required array keys (the scan may
+ * not even have a required array).  These callers should just pass a NULL
+ * pstate (since there is never any question of stopping the scan).  No call
+ * to _bt_tuple_before_array_skeys is required ahead of these calls.
  *
  * Note that we deal with non-array required equality strategy scan keys as
  * degenerate single element arrays here.  Obviously, they can never really
@@ -1469,8 +1470,8 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		pstate->firstmatch = false;
 
-		/* Shouldn't have to invalidate 'prechecked', though */
-		Assert(!pstate->prechecked);
+		/* Shouldn't have to invalidate 'prechecked' or 'skipskip' or 'ikey' */
+		Assert(!pstate->prechecked && !pstate->skipskip && pstate->ikey == 0);
 
 		/*
 		 * Once we return we'll have a new set of required array keys, so
@@ -1522,8 +1523,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -1670,7 +1669,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -1712,7 +1711,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -1727,7 +1726,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -1798,7 +1797,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -2205,13 +2204,14 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	TupleDesc	tupdesc = RelationGetDescr(scan->indexRelation);
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ScanDirection dir = so->currPos.dir;
-	int			ikey = 0;
+	int			ikey = pstate->ikey;
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+							arrayKeys, pstate->skipskip,
+							pstate->prechecked, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
@@ -2223,21 +2223,22 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
 		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
+		Assert(!pstate->skipskip && !pstate->prechecked &&
+			   !pstate->firstmatch);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
 	if (pstate->prechecked || pstate->firstmatch)
 	{
 		bool		dcontinuescan;
-		int			dikey = 0;
+		int			dikey = pstate->ikey;
 
 		/*
 		 * Call relied on continuescan/firstmatch prechecks -- assert that we
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, pstate->skipskip, false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -2260,6 +2261,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * It's also possible that the scan is still _before_ the _start_ of
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
+	Assert(!pstate->skipskip);
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
@@ -2368,7 +2370,7 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	Assert(so->numArrayKeys);
 
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -2407,17 +2409,24 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Though we advance non-required array keys on our own, that shouldn't have
  * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
+ * have no fixed relationship with the scan's progress.  (skipskip=true makes
+ * us treat required scan keys as non-required, though.  That's safe because
+ * _bt_readpage will account for our failure to fully maintain the scan's
+ * arrays later on, right before its finaltup call to _bt_checkkeys.)
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass skipskip=true to instruct us to skip all of the scan's skip arrays.
+ * This provides the scan with a way of keeping the cost of maintaining its
+ * skip arrays under control, given skip arrays on high cardinality columns
+ * (i.e. given a skip array on a column which isn't a good fit for skip scan).
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool skipskip,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -2434,10 +2443,18 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when reading a page that's now using the "skipskip" optimization)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (skipskip)
+		{
+			Assert(!prechecked);
+
+			if (key->sk_flags & SK_BT_SKIP)
+				continue;
+		}
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
@@ -2495,7 +2512,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
-									 continuescan))
+									 skipskip, continuescan))
 				continue;
 			return false;
 		}
@@ -2550,7 +2567,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -2568,7 +2585,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -2637,7 +2654,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
  */
 static bool
 _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
-					 TupleDesc tupdesc, ScanDirection dir, bool *continuescan)
+					 TupleDesc tupdesc, ScanDirection dir, bool skipskip,
+					 bool *continuescan)
 {
 	ScanKey		subkey = (ScanKey) DatumGetPointer(skey->sk_argument);
 	int32		cmpresult = 0;
@@ -2677,7 +2695,11 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 
 		if (isNull)
 		{
-			if (subkey->sk_flags & SK_BT_NULLS_FIRST)
+			if (skipskip)
+			{
+				/* treat scan key as non-required during skipskip calls */
+			}
+			else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -2731,8 +2753,12 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			 */
 			Assert(subkey != (ScanKey) DatumGetPointer(skey->sk_argument));
 			subkey--;
-			if ((subkey->sk_flags & SK_BT_REQFWD) &&
-				ScanDirectionIsForward(dir))
+			if (skipskip)
+			{
+				/* treat scan key as non-required during skipskip calls */
+			}
+			else if ((subkey->sk_flags & SK_BT_REQFWD) &&
+					 ScanDirectionIsForward(dir))
 				*continuescan = false;
 			else if ((subkey->sk_flags & SK_BT_REQBKWD) &&
 					 ScanDirectionIsBackward(dir))
@@ -2784,7 +2810,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			break;
 	}
 
-	if (!result)
+	if (!result && !skipskip)
 	{
 		/*
 		 * Tuple fails this qual.  If it's a required qual for the current
@@ -2828,6 +2854,8 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	Assert(!pstate->skipskip);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
@@ -2888,6 +2916,164 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	}
 }
 
+/*
+ * Determine if a scan with skip array keys should avoid evaluating its skip
+ * arrays, plus any scan keys covering a prefix of unchanging attribute values
+ * on caller's page.
+ *
+ * Called at the start of a _bt_readpage call for the second or subsequent
+ * leaf page scanned by a primitive index scan (that uses skip scan).
+ *
+ * Sets pstate.skipskip when it's safe for _bt_readpage caller to apply the
+ * 'skipskip' optimization on this page during skip scans.
+ */
+void
+_bt_checkkeys_skipskip(IndexScanDesc scan, BTReadPageState *pstate)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	ScanDirection dir = so->currPos.dir;
+	ItemId		iid;
+	IndexTuple	firsttup,
+				lasttup;
+	int			arrayidx = 0,
+				set_ikey = 0,
+				firstunequalattnum;
+	bool		nonsharedprefix_nonskip = false,
+				nonsharedprefix_range_skip = false,
+				ikeyset = false;
+
+	/* Only called during skip scans */
+	Assert(so->skipScan && !pstate->skipskip);
+
+	/*
+	 * If this is a forward scan, and the previous page's high key didn't have
+	 * a truncated lower-order attribute with a required scan key, then don't
+	 * apply the optimization.
+	 */
+	if (!so->scanBehind && ScanDirectionIsForward(dir))
+		return;
+
+	/* Can't combine 'skipskip' with the similar 'precheck' optimization */
+	Assert(!pstate->prechecked);
+
+	Assert(pstate->minoff < pstate->maxoff);
+	if (ScanDirectionIsForward(dir))
+	{
+		iid = PageGetItemId(pstate->page, pstate->minoff);
+		firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+		iid = PageGetItemId(pstate->page, pstate->maxoff);
+		lasttup = (IndexTuple) PageGetItem(pstate->page, iid);
+	}
+	else
+	{
+		iid = PageGetItemId(pstate->page, pstate->maxoff);
+		firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+		iid = PageGetItemId(pstate->page, pstate->minoff);
+		lasttup = (IndexTuple) PageGetItem(pstate->page, iid);
+	}
+
+	Assert(!BTreeTupleIsPivot(firsttup) && !BTreeTupleIsPivot(lasttup));
+	Assert(firsttup != lasttup);
+
+	firstunequalattnum = 1;
+	if (so->numberOfKeys > 2)
+		firstunequalattnum = _bt_keep_natts_fast(rel, firsttup, lasttup);
+
+	for (int ikey = 0; ikey < so->numberOfKeys; ikey++)
+	{
+		ScanKey		cur = so->keyData + ikey;
+		BTArrayKeyInfo *array = NULL;
+		Datum		tupdatum;
+		bool		tupnull;
+		int32		result;
+
+		if (cur->sk_attno >= firstunequalattnum)
+		{
+			if (!ikeyset || !nonsharedprefix_nonskip)
+			{
+				ikeyset = true;
+				set_ikey = ikey;
+			}
+			if (!(cur->sk_flags & SK_BT_SKIP))
+				nonsharedprefix_nonskip = true;
+		}
+
+		if (cur->sk_strategy != BTEqualStrategyNumber)
+			break;
+		if (!(cur->sk_flags & SK_SEARCHARRAY))
+		{
+			if (cur->sk_attno < firstunequalattnum)
+			{
+				tupdatum = index_getattr(firsttup, cur->sk_attno, tupdesc, &tupnull);
+
+				/* Scankey has a valid/comparable sk_argument value */
+				result = _bt_compare_array_skey(&so->orderProcs[ikey],
+												tupdatum, tupnull,
+												cur->sk_argument, cur);
+				if (result != 0)
+				{
+					ikeyset = true;
+					nonsharedprefix_nonskip = true;
+				}
+
+			}
+			continue;
+		}
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == ikey);
+
+		/*
+		 * Only need to maintain set_ikey and current so->arrayKeys[] offset,
+		 * unless dealing with a range skip array
+		 */
+		if (array->num_elems != -1 || array->null_elem)
+			continue;
+
+		/*
+		 * Found a range skip array to test.
+		 *
+		 * If this is the first range skip array, it is still safe to apply
+		 * the skipskip optimization.  If this is the second or subsequent
+		 * range skip array, then it is only safe if there is no more than one
+		 * range skip array on an attribute whose values change on this page.
+		 *
+		 * Note: we deliberately ignore regular (non-range) skip arrays, since
+		 * they're always satisfied by any possible attribute value.
+		 */
+		if (nonsharedprefix_range_skip)
+			return;
+		if (cur->sk_attno < firstunequalattnum)
+		{
+			/*
+			 * This range skip array doesn't count towards our "no more than
+			 * one range skip array" limit -- but it must still be satisfied
+			 * by both firsttup and finaltup
+			 */
+		}
+		else
+			nonsharedprefix_range_skip = true;
+
+		/* Test the first/lower bound non-pivot tuple on the page */
+		tupdatum = index_getattr(firsttup, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], false, dir,
+								   tupdatum, tupnull, array, cur, &result);
+		if (result != 0)
+			return;
+
+		/* Test the page's finaltup (unless attribute is truncated) */
+		tupdatum = index_getattr(lasttup, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], false, dir,
+								   tupdatum, tupnull, array, cur, &result);
+		if (result != 0)
+			return;
+	}
+
+	pstate->ikey = set_ikey;
+	pstate->skipskip = true;
+}
+
 /*
  * _bt_killitems - set LP_DEAD state for items an indexscan caller has
  * told us were killed
-- 
2.47.2

v22-0002-Add-nbtree-skip-scan-optimizations.patchapplication/octet-stream; name=v22-0002-Add-nbtree-skip-scan-optimizations.patchDownload
From 1bc4563e294fd6c33aaec3640fe963f1fffe35b9 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v22 2/5] Add nbtree skip scan optimizations.

Teach nbtree composite index scans to opportunistically skip over
irrelevant sections of the index.  This is possible whenever the scan
encounters a group of tuples that all share a common prefix of the same
index column value.  Each such group of tuples can be thought of as a
"logical subindex".  The scan exhaustively "searches every subindex".
Scans of composite indexes with a leading low cardinality column can now
skip over irrelevant sections of the index, which is far more efficient.

When nbtree is passed input scan keys derived from a query predicate
"WHERE b = 5", new nbtree preprocessing steps now output scan keys for
"WHERE a = ANY(<every possible 'a' value>) AND b = 5".  That is, nbtree
preprocessing generates a "skip array" (and an associated scan key) for
the omitted column "a".  This enables marking the scan key on "b" as
required to continue the scan.  This approach builds on the design for
ScalarArrayOp scans established by commit 5bf748b8: skip arrays generate
their values procedurally, but otherwise work just like SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values are conceptually just like any
other value that might appear in an array, though they never actually
find exact matches in any index tuple.

Inequality scan keys can affect how skip arrays generate their values.
Their range is constrained by the inequalities.  For example, a skip
array on "a" will only ever use element values 1 and 2 given a qual
"WHERE a BETWEEN 1 AND 2 AND b = 66".  A scan using such a skip array
has almost identical performance characteristics to a scan with the qual
"WHERE a = ANY('{1, 2}') AND b = 66".  This will be much faster when the
scan can be executed as two highly selective primitive index scans,
rather than a single much larger scan that has to read many more leaf
pages.  It isn't guaranteed to improve performance, though.

Preprocessing is optimistic about skipping working out: it applies
simple static rules to decide where to generate skip arrays.  It's up to
the scan to keep the overhead of maintaining its arrays under control
when skipping isn't helpful.  This partly relies on the same set of
behaviors (introduced in commit 5bf748b8) that make ScalarArrayOp scans
perform well with large arrays of clustered values.  That isn't enough
to avoid significant regressions, though.  A later commit will go
further by teaching scan related code to notice when skipping isn't
working out, and then having most calls to _bt_checkkeys avoid the
overhead of maintaining the scan's skip arrays.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |   3 +-
 src/include/access/nbtree.h                   |  30 +-
 src/include/catalog/pg_amproc.dat             |  22 +
 src/include/catalog/pg_proc.dat               |  27 +
 src/include/storage/lwlock.h                  |   1 +
 src/include/utils/skipsupport.h               |  98 +++
 src/backend/access/index/indexam.c            |   3 +-
 src/backend/access/nbtree/nbtcompare.c        | 273 +++++++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 597 ++++++++++++--
 src/backend/access/nbtree/nbtree.c            | 211 ++++-
 src/backend/access/nbtree/nbtsearch.c         | 107 ++-
 src/backend/access/nbtree/nbtutils.c          | 738 ++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |   4 +
 src/backend/commands/opclasscmds.c            |  25 +
 src/backend/storage/lmgr/lwlock.c             |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/date.c                  |  46 ++
 src/backend/utils/adt/meson.build             |   1 +
 src/backend/utils/adt/selfuncs.c              | 403 +++++++---
 src/backend/utils/adt/skipsupport.c           |  61 ++
 src/backend/utils/adt/timestamp.c             |  48 ++
 src/backend/utils/adt/uuid.c                  |  70 ++
 doc/src/sgml/btree.sgml                       |  34 +-
 doc/src/sgml/indexam.sgml                     |   3 +-
 doc/src/sgml/indices.sgml                     |  49 +-
 doc/src/sgml/xindex.sgml                      |  16 +-
 src/test/regress/expected/alter_generic.out   |  10 +-
 src/test/regress/expected/create_index.out    |   4 +-
 src/test/regress/expected/psql.out            |   3 +-
 src/test/regress/sql/alter_generic.sql        |   5 +-
 src/test/regress/sql/create_index.sql         |   4 +-
 src/tools/pgindent/typedefs.list              |   3 +
 33 files changed, 2568 insertions(+), 334 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index fb94b3d1a..5046b31f3 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -206,7 +206,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 6a501537e..d5e01692c 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -707,6 +708,10 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
  *	(BTOPTIONS_PROC).  These procedures define a set of user-visible
  *	parameters that can be used to control operator class behavior.  None of
  *	the built-in B-Tree operator classes currently register an "options" proc.
+ *
+ *	To facilitate more efficient B-Tree skip scans, an operator class may
+ *	choose to offer a sixth amproc procedure (BTSKIPSUPPORT_PROC).  For full
+ *	details, see src/include/utils/skipsupport.h.
  */
 
 #define BTORDER_PROC		1
@@ -714,7 +719,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1027,10 +1033,21 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
-	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+	int			cur_elem;		/* index of current element in elem_values */
+
+	/* fields for skip arrays, which generate their elements procedurally */
+	int16		attlen;			/* attr len in bytes */
+	bool		attbyval;		/* as FormData_pg_attribute.attbyval */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupport sksup;			/* skip support (NULL if opclass lacks it) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1042,6 +1059,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Last array advancement matched -inf attr? */
 	bool		oppositeDirCheck;	/* explicit scanBehind recheck needed? */
@@ -1118,6 +1136,10 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on omitted prefix column */
+#define SK_BT_MINMAXVAL	0x00080000	/* lowest/highest key in array's range */
+#define SK_BT_NEXT		0x00100000	/* positions the scan > sk_argument */
+#define SK_BT_PRIOR		0x00200000	/* positions the scan < sk_argument */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1164,7 +1186,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 508f48d34..e9194e68e 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 18560755d..700f16e49 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2260,6 +2275,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4447,6 +4465,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6303,6 +6324,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9351,6 +9375,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 2aa46fd50..6803d5b9e 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..6fc82d02e
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining pairs of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code falls back on next-key sentinel values for any opclass that
+ * doesn't provide its own skip support function.  There is no benefit to
+ * providing skip support for an opclass on a type where guessing that the
+ * next indexed value is the next possible indexable value seldom works out.
+ * It might not even be feasible to add skip support for a continuous type.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order)
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 *
+	 * Operator class decrement/increment functions never have to directly
+	 * deal with the value NULL, nor with ASC vs. DESC column ordering.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern SkipSupport PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+												 bool reverse);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 8b1f55543..0e62d7a7a 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 291cb8fc1..4da5a3c1d 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 1fd1da5f1..a9f6d896c 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -45,8 +45,15 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
+static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
+								 ScanKey skey, FmgrInfo *orderproc,
+								 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
+								 BTArrayKeyInfo *array, bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
+							   int *numSkipArrayKeys);
 static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
 									  Oid elemtype, StrategyNumber strat,
 									  Datum *elems, int nelems);
@@ -89,6 +96,8 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -101,10 +110,16 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never generated skip array scan keys, it would be possible for "gaps"
+ * to appear that make it unsafe to mark any subsequent input scan keys
+ * (copied from scan->keyData[]) as required to continue the scan.  Prior to
+ * Postgres 18, a qual like "WHERE y = 4" always required a full index scan.
+ * It'll now become "WHERE x = ANY('{every possible x value}') and y = 4" on
+ * output, due to the addition of a skip array on "x".  This has the potential
+ * to be much more efficient than a full index scan (though it'll behave like
+ * a full index scan when there are many distinct values for "x").
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -141,7 +156,12 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -200,6 +220,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProcs[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -231,6 +259,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
+		Assert(!so->skipScan);
 
 		return;
 	}
@@ -288,7 +317,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -345,7 +375,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -393,6 +422,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, _bt_preprocess_array_keys has already added "=" keys
+			 * sufficient to form an unbroken series of "=" constraints on all
+			 * attrs prior to the attr from the final scan->keyData[] key.
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -481,6 +515,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -489,6 +524,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -508,8 +544,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -803,6 +837,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -812,6 +849,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -885,6 +938,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -978,24 +1032,55 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
  * Compare an array scan key to a scalar scan key, eliminating contradictory
  * array elements such that the scalar scan key becomes redundant.
  *
+ * If the opfamily is incomplete we may not be able to determine which
+ * elements are contradictory.  When we return true we'll have validly set
+ * *qual_ok, guaranteeing that at least the scalar scan key can be considered
+ * redundant.  We return false if the comparison could not be made (caller
+ * must keep both scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar inequality scan keys.
+ */
+static bool
+_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
+							   bool *qual_ok)
+{
+	Assert(arraysk->sk_attno == skey->sk_attno);
+	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
+		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
+	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
+		   skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Just call the appropriate helper function based on whether it's a SAOP
+	 * array or a skip array.  Both helpers will set *qual_ok in passing.
+	 */
+	if (array->num_elems != -1)
+		return _bt_saoparray_shrink(scan, arraysk, skey, orderproc, array,
+									qual_ok);
+	else
+		return _bt_skiparray_shrink(scan, skey, array, qual_ok);
+}
+
+/*
+ * Preprocessing of SAOP (non-skip) array scan key, used to determine which
+ * array elements are eliminated as contradictory by a non-array scalar key.
+ * _bt_compare_array_scankey_args helper function.
+ *
  * Array elements can be eliminated as contradictory when excluded by some
  * other operator on the same attribute.  For example, with an index scan qual
  * "WHERE a IN (1, 2, 3) AND a < 2", all array elements except the value "1"
  * are eliminated, and the < scan key is eliminated as redundant.  Cases where
  * every array element is eliminated by a redundant scalar scan key have an
  * unsatisfiable qual, which we handle by setting *qual_ok=false for caller.
- *
- * If the opfamily doesn't supply a complete set of cross-type ORDER procs we
- * may not be able to determine which elements are contradictory.  If we have
- * the required ORDER proc then we return true (and validly set *qual_ok),
- * guaranteeing that at least the scalar scan key can be considered redundant.
- * We return false if the comparison could not be made (caller must keep both
- * scan keys when this happens).
  */
 static bool
-_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
-							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
-							   bool *qual_ok)
+_bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+					 FmgrInfo *orderproc, BTArrayKeyInfo *array, bool *qual_ok)
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
@@ -1006,14 +1091,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
-	Assert(arraysk->sk_attno == skey->sk_attno);
 	Assert(array->num_elems > 0);
-	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
-		   arraysk->sk_strategy == BTEqualStrategyNumber);
-	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
-		   skey->sk_strategy != BTEqualStrategyNumber);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
 
 	/*
 	 * _bt_binsrch_array_skey searches an array for the entry best matching a
@@ -1112,6 +1191,99 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	return true;
 }
 
+/*
+ * Preprocessing of skip (non-SAOP) array scan key, used to determine
+ * redundancy against a non-array scalary scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function.
+ *
+ * Unlike _bt_saoparray_shrink, we cannot really modify caller's array
+ * in-place.  Skip arrays work by procedurally generating their elements as
+ * needed, so our approach is to store a copy of the inequality in the skip
+ * array.  This forces the array's elements to be generated within the limits
+ * of a range that's described/constrained by the array's inequalities.
+ */
+static bool
+_bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
+					 bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Array must not generate a NULL array element (for "IS NULL" qual).  Its
+	 * index attribute is constrained by a strict operator, so NULL elements
+	 * must not be returned by the scan (it would be wrong to allow it).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Consider if we should treat caller's scalar scan key as the skip
+	 * array's high_compare or low_compare.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way scan code can generally assume that
+	 * it'll always be safe to use the input-opclass-only-type procs stored in
+	 * so->orderProcs[] (they can only be cross-type for a SAOP array key).
+	 * The only exception is code that deal with MINMAXVAL = array scan keys.
+	 * When an array scan key is marked MINMAXVAL, it won't have a valid datum
+	 * in its sk_argument.  The scan must directly apply underlying array's
+	 * low_compare and/or high_compare in an ad-hoc way instead.
+	 *
+	 * This approach avoids problems when the inequality sk_argument is of a
+	 * different type to the array's = scan key's sk_argument.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* discard new high_compare, keep old one */
+
+				/* replace old high_compare with caller's new one */
+			}
+
+			/* caller's high_compare becomes skip array's high_compare */
+			array->high_compare = skey;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* discard new low_compare, keep old one */
+
+				/* replace old low_compare with caller's new one */
+			}
+
+			/* caller's low_compare becomes skip array's low_compare */
+			array->low_compare = skey;
+			break;
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1137,6 +1309,12 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -1152,41 +1330,36 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	Oid			skip_eq_ops[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
-	ScanKey		cur;
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
+	so->skipScan = (numSkipArrayKeys > 0);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys we'll need to generate below
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -1201,18 +1374,20 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
+		ScanKey		inkey = scan->keyData + input_ikey,
+					cur;
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
 		Oid			elemtype;
@@ -1225,21 +1400,114 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		Datum	   *elem_values;
 		bool	   *elem_nulls;
 		int			num_nonnulls;
-		int			j;
+
+		/* set up next output scan key */
+		cur = &arrayKeyData[numArrayKeyData];
+
+		/* Backfill skip arrays for attrs < or <= input key's attr? */
+		while (numSkipArrayKeys && attno_skip <= inkey->sk_attno)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skip_eq_ops[attno_skip - 1];
+			CompactAttribute *attr;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/*
+				 * Attribute already has an = input key, so don't output a
+				 * skip array for attno_skip.  Just copy attribute's = input
+				 * key into arrayKeyData[] once outside this inner loop.
+				 *
+				 * Note: When we get here there must be an additional index
+				 * attribute that lacks an equality input key, and still needs
+				 * a skip array (numSkipArrayKeys would be 0 if there wasn't).
+				 */
+				Assert(attno_skip == inkey->sk_attno);
+				/* inkey can't be last input key to be marked required: */
+				Assert(input_ikey < scan->numberOfKeys - 1);
+#if 0
+				/* Could be a redundant input scan key, so can't do this: */
+				Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+					   (inkey->sk_flags & SK_SEARCHNULL));
+#endif
+
+				attno_skip++;
+				break;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize generic BTArrayKeyInfo fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+
+			/* Initialize skip array specific BTArrayKeyInfo fields */
+			attr = TupleDescCompactAttr(RelationGetDescr(rel), attno_skip - 1);
+			reverse = (indoption[attno_skip - 1] & INDOPTION_DESC) != 0;
+			so->arrayKeys[numArrayKeys].attlen = attr->attlen;
+			so->arrayKeys[numArrayKeys].attbyval = attr->attbyval;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup =
+				PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse);
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * We'll need a 3-way ORDER proc to perform "binary searches" for
+			 * the next matching array element.  Set that up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			numArrayKeys++;
+			numArrayKeyData++;	/* keep this scan key/array */
+
+			/* set up next output scan key */
+			cur = &arrayKeyData[numArrayKeyData];
+
+			/* remember having output this skip array and scan key */
+			numSkipArrayKeys--;
+			attno_skip++;
+		}
 
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
-		*cur = scan->keyData[input_ikey];
+		*cur = *inkey;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -1257,7 +1525,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * all btree operators are strict.
 		 */
 		num_nonnulls = 0;
-		for (j = 0; j < num_elems; j++)
+		for (int j = 0; j < num_elems; j++)
 		{
 			if (!elem_nulls[j])
 				elem_values[num_nonnulls++] = elem_values[j];
@@ -1295,7 +1563,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -1306,7 +1574,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -1323,7 +1591,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -1392,23 +1660,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			origelemtype = elemtype;
 		}
 
-		/*
-		 * And set up the BTArrayKeyInfo data.
-		 *
-		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
-		 * scan_key field later on, after so->keyData[] has been finalized.
-		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		/* Initialize generic BTArrayKeyInfo fields */
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
+
+		/* Initialize SAOP array specific BTArrayKeyInfo fields */
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].cur_elem = -1;	/* i.e. invalid */
+
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
+	Assert(numSkipArrayKeys == 0);
+
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -1514,7 +1783,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -1565,7 +1835,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -1575,6 +1845,189 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit every so->arrayKeys[] entry.
+ *
+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller must add this many
+ * skip arrays to each of the most significant attributes lacking any keys
+ * that use the = strategy (IS NULL keys count as = keys here).  The specific
+ * attributes that need skip arrays are indicated by initializing caller's
+ * skip_eq_ops[] 0-based attribute offset to a valid = op strategy Oid.  We'll
+ * only ever set skip_eq_ops[] entries to InvalidOid for attributes that
+ * already have an equality key in scan->keyData[] input keys -- and only when
+ * there's some later "attribute gap" for us to "fill-in" with a skip array.
+ *
+ * We're optimistic about skipping working out: we always add exactly the skip
+ * arrays needed to maximize the number of input scan keys that can ultimately
+ * be marked as required to continue the scan (but no more).  For a composite
+ * index on (a, b, c, d), we'll instruct caller to add skip arrays as follows:
+ *
+ * Input keys						Output keys (after all preprocessing)
+ * ----------						-------------------------------------
+ * a = 1:							a = 1 (no skip arrays)
+ * a = ANY('{0, 1}'):				a = ANY('{0, 1}') (no skip arrays)
+ * b = 42:							skip a AND b = 42
+ * b = ANY('{40, 42}'):				skip a AND b = ANY('{40, 42}')
+ * a = 1 AND b = 42:				a = 1 AND b = 42 (no skip arrays)
+ * a >= 1 AND b = 42:				range skip a AND b = 42
+ * a = 1 AND b > 42:				a = 1 AND b > 42 (no skip arrays)
+ * a >= 1 AND a <= 3 AND b = 42:	range skip a AND b = 42
+ * a = 1 AND c <= 27:				a = 1 AND skip b AND c <= 27
+ * a = 1 AND d >= 1:				a = 1 AND skip b AND skip c AND d >= 1
+ * a = 1 AND b >= 42 AND d > 1:		a = 1 AND range skip b AND skip c AND d > 1
+ */
+static int
+_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_skip = 1,
+				attno_inkey = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+#ifdef DEBUG_DISABLE_SKIP_SCAN
+	/* don't attempt to add skip arrays */
+	return numArrayKeys;
+#endif
+
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+			/* Look up input opclass's equality operator (might fail) */
+			skip_eq_ops[attno_skip - 1] =
+				get_opfamily_member(opfamily, opcintype, opcintype,
+									BTEqualStrategyNumber);
+			if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key
+		 * (doesn't matter whether that attribute has an equality key, or just
+		 * uses inequality keys).
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys
+			 */
+			if (attno_has_equal)
+			{
+				/* Attributes with an = key must have InvalidOid eq_op set */
+				skip_eq_ops[attno_skip - 1] = InvalidOid;
+			}
+			else
+			{
+				Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+				Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+				/* Look up input opclass's equality operator (might fail) */
+				skip_eq_ops[attno_skip - 1] =
+					get_opfamily_member(opfamily, opcintype, opcintype,
+										BTEqualStrategyNumber);
+
+				if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+				{
+					/*
+					 * Input opclass lacks an equality strategy operator, so
+					 * don't generate a skip array that definitely won't work
+					 */
+					break;
+				}
+
+				/* plan on adding a backfill skip array for this attribute */
+				(*numSkipArrayKeys)++;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required later on.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
 /*
  * _bt_find_extreme_element() -- get least or greatest array element
  *
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 6345a37a8..38d848994 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -29,6 +29,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -70,7 +71,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -78,11 +79,24 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 *
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -409,6 +423,7 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 		memcpy(scan->keyData, scankey, scan->numberOfKeys * sizeof(ScanKeyData));
 	so->numberOfKeys = 0;		/* until _bt_preprocess_keys sets it */
 	so->numArrayKeys = 0;		/* ditto */
+	so->skipScan = false;		/* tracks if skip arrays are in use */
 }
 
 /*
@@ -535,10 +550,155 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume all input scankeys will be output with its own
+	 * SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * scan array (and associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		CompactAttribute *attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescCompactAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array/skip scan key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & SK_BT_MINMAXVAL)
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   array->attbyval, array->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array/skip scan key, while freeing memory allocated
+		 * for old sk_argument where required
+		 */
+		if (!array->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & SK_BT_MINMAXVAL)
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -549,7 +709,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -572,16 +733,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -608,6 +769,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -652,7 +814,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -674,14 +836,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -719,7 +876,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -763,11 +920,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -806,7 +963,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -815,7 +972,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -833,6 +990,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -842,7 +1000,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -852,14 +1010,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 941b4eaaf..947608945 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -964,6 +964,15 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  All
+	 * array keys are always considered = keys, but we'll sometimes need to
+	 * treat the current key value as if we were using an inequality strategy.
+	 * This happens whenever a skip array's scan key is found to have been set
+	 * to one of the special sentinel values representing an imaginary point
+	 * before/after some (or all) of the index's real indexable values.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1039,8 +1048,49 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is MINMAXVAL, just choose low_compare/high_compare
+				 * (or consider if array implies a NOT NULL constraint).
+				 *
+				 * Note: if the array's low_compare key makes 'chosen' NULL,
+				 * then we behave as if the array's first element is -inf,
+				 * except when !array->null_elem, which implies a usable NOT
+				 * NULL constraint (or an explicit NOT NULL input key).
+				 */
+				if (chosen != NULL && (chosen->sk_flags & SK_BT_MINMAXVAL))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skiparraysk = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					Assert(so->skipScan);
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+						chosen = array->low_compare;
+					else
+						chosen = array->high_compare;
+
+					Assert(chosen == NULL ||
+						   chosen->sk_attno == skiparraysk->sk_attno);
+
+					if (!array->null_elem)
+						impliesNN = skiparraysk;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1083,9 +1133,44 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 
 				/*
-				 * Done if that was the last attribute, or if next key is not
-				 * in sequence (implying no boundary key is available for the
-				 * next attribute).
+				 * If this is a scan key for a skip array whose current
+				 * element is marked NEXT or PRIOR, adjust strat_total
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(so->skipScan);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(strat_total == BTEqualStrategyNumber);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no reason to
+					 * save any later would-be boundary keys in startKeys[].
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).
+					 */
+					break;
+				}
+
+				/*
+				 * Done if that was the last index attribute.  Also done if
+				 * there is a gap index attribute that lacks any usable keys
+				 * (only possible in edge cases where preprocessing could not
+				 * generate a skip array key that "fills the gap").
 				 */
 				if (i >= so->numberOfKeys ||
 					cur->sk_attno != curattr + 1)
@@ -1576,10 +1661,12 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * corresponding value from the last item on the page.  So checking with
 	 * the last item on the page would give a more precise answer.
 	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
+	 * We don't do this for the first page read by each (primitive) scan, to
+	 * avoid slowing down point queries.  They typically don't stand to gain
+	 * much when the optimization can be applied, and are more likely to
+	 * notice the overhead of the precheck.  Also avoid it during skip scans,
+	 * where individual primitive scans that don't just read one leaf page are
+	 * likely to advance their skip array(s) several times per leaf page read.
 	 *
 	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
 	 * just set a low-order required array's key to the best available match
@@ -1603,7 +1690,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!firstPage && !so->scanBehind && minoff < maxoff)
+	if (!firstPage && !so->skipScan && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 693e43c67..dd0f18234 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -30,6 +30,19 @@
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+									   bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									  int32 result, Datum tupdatum, bool tupnull);
+static void _bt_skiparray_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_array_set_low_or_high(Relation rel, ScanKey skey,
+									  BTArrayKeyInfo *array, bool low_not_high);
+static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -205,6 +218,7 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 	int32		result = 0;
 
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
+	Assert(!(cur->sk_flags & SK_BT_MINMAXVAL));
 
 	if (tupnull)				/* NULL tupdatum */
 	{
@@ -281,6 +295,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -403,6 +419,191 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, because the array doesn't really
+ * have any elements (it generates its array elements procedurally instead).
+ * Note that non-range skip arrays have an element for the value NULL, which
+ * is applied as an IS NULL qual within _bt_checkkeys.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  Other values indicate what _bt_compare_array_skey returned
+ * for the best available match to tupdatum/tupnull (in practice this means
+ * either the lowest item or the highest item in the range of the array).
+ * result must be passed to _bt_skiparray_set_element when advancing array.
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ *
+ * Note: This function doesn't actually binary search.  It uses an interface
+ * that's as similar to _bt_binsrch_array_skey as possible for consistency.
+ * "Binary searching a skip array" just means determining if tupdatum/tupnull
+ * are before, within, or beyond the range of caller's skip array.
+ */
+static void
+_bt_binsrch_skiparray_skey(FmgrInfo *orderproc,
+						   bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (array->null_elem)
+			*set_elem_result = 0;	/* NULL "=" NULL */
+		else if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
+/*
+ * _bt_skiparray_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Sets scan key's sk_argument based on tupdatum/tupnull args iff result == 0,
+ * since that indicates that _bt_binsrch_skiparray_skey reported args to be
+ * within the range of the array.  Otherwise sets scan key to the array's
+ * lowest or highest element according to which is closest to tupdatum/tupnull
+ * result from caller's _bt_binsrch_skiparray_skey.
+ */
+static void
+_bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  int32 result, Datum tupdatum, bool tupnull)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (result)
+	{
+		/* tupdatum/tupnull is out of the range of the skip array */
+		Assert(!array->null_elem);
+		_bt_array_set_low_or_high(rel, skey, array, result < 0);
+		return;
+	}
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR);
+	Assert(!(tupnull && !array->null_elem));
+	if (!tupnull)
+		skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen);
+	else
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
+/*
+ * _bt_skiparray_unset_isnull() -- increment/decrement skip array scan key
+ * 								   from NULL
+ *
+ * Unsets scan key's "IS NULL" marking, and sets the non-NULL value from the
+ * array immediately before (or immediate after) NULL in the key space.
+ */
+static void
+_bt_skiparray_unset_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(skey->sk_flags & SK_SEARCHNULL);
+	Assert(skey->sk_flags & SK_ISNULL);
+	Assert(!(skey->sk_flags & (SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(skey->sk_argument == 0);
+	Assert(array->null_elem && !array->low_compare && !array->high_compare);
+
+	/*
+	 * sk_argument must be set to whatever non-NULL value comes immediately
+	 * before or after NULL
+	 */
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+	if (skey->sk_flags & SK_BT_NULLS_FIRST)
+		skey->sk_argument = datumCopy(array->sksup->low_elem,
+									  array->attbyval, array->attlen);
+	else
+		skey->sk_argument = datumCopy(array->sksup->high_elem,
+									  array->attbyval, array->attlen);
+}
+
+/*
+ * _bt_skiparray_set_isnull() -- decrement/increment skip array scan key to
+ * 								 NULL
+ */
+static void
+_bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_SEARCHNULL | SK_ISNULL |
+							   SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+	Assert(array->null_elem);
+	Assert(!array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -412,29 +613,343 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_array_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_array_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  bool low_not_high)
+{
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else
+	{
+		/* Setting skip array to lowest/highest non-NULL element */
+		skey->sk_flags |= SK_BT_MINMAXVAL;
+	}
+}
+
+/*
+ * _bt_array_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_MINMAXVAL));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the minimum value within the range
+	 * of a skip array (often just -inf) is never decrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINMAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support decrement the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Decrement current array element to the high_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_skiparray_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup->decrement(rel, skey->sk_argument, &uflow);
+	if (unlikely(uflow))
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_skiparray_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_array_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+		Assert(!(skey->sk_flags & SK_BT_MINMAXVAL));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the maximum value within the range
+	 * of a skip array (often just +inf) is never incrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINMAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support increment the scan key's current element
+	 * using a callback
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * Increment current array element to the low_elem value provided by
+		 * opclass skip support routine.
+		 */
+		_bt_skiparray_unset_isnull(rel, skey, array);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup->increment(rel, skey->sk_argument, &oflow);
+	if (unlikely(oflow))
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_skiparray_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -450,6 +965,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -459,29 +975,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_array_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_array_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -541,6 +1058,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -548,7 +1066,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -560,16 +1077,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_array_set_low_or_high(rel, cur, array,
+								  ScanDirectionIsForward(dir));
 	}
 }
 
@@ -694,9 +1205,71 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & SK_BT_MINMAXVAL))
+		{
+			/* Scankey has a valid/comparable sk_argument value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0)
+			{
+				/*
+				 * Interpret result in a way that takes NEXT/PRIOR into
+				 * account
+				 */
+				if (cur->sk_flags & SK_BT_NEXT)
+					result = -1;
+				else if (cur->sk_flags & SK_BT_PRIOR)
+					result = 1;
+
+				Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP));
+			}
+		}
+		else
+		{
+			/*
+			 * Current array element/array = scan key value is a sentinel
+			 * value that represents the lowest (or highest) possible value
+			 * that's still within the range of the array.
+			 *
+			 * We don't have a valid sk_argument value from = scan key.  Check
+			 * if tupdatum is within the range of skip array instead.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(&so->orderProcs[ikey], true, -dir,
+									   tupdatum, tupnull, array, cur,
+									   &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum satisfies both low_compare and high_compare, so
+				 * it's time to advance the array keys.
+				 *
+				 * Note: It's possible that the skip array will "advance" from
+				 * its current MINMAXVAL representation to an alternative
+				 * representation where the = key gets a useful sk_argument,
+				 * even though both representations are logically equivalent.
+				 * This is possible when low_compare uses the <= strategy, and
+				 * when high_compare uses the >= strategy.
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1032,18 +1605,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1068,18 +1632,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -1097,12 +1652,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(&so->orderProcs[ikey],
+										   cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -1178,11 +1742,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+		if (array)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->num_elems == -1)
+			{
+				/* Skip array "binary search" result determines new element */
+				_bt_skiparray_set_element(rel, cur, array, result,
+										  tupdatum, tupnull);
+			}
+			else if (array->cur_elem != set_elem)
+			{
+				/* SAOP array has new set_elem (returned by binary search) */
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
 		}
 	}
 
@@ -1575,10 +2149,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -1901,6 +2476,21 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key uses one of several sentinel values (usually
+		 * only at the start or end of each primitive index scan).  We always
+		 * fall back on _bt_tuple_before_array_skeys to decide what to do.
+		 */
+		if (key->sk_flags & (SK_BT_MINMAXVAL | SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index b87c959a2..c2dd458c8 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -114,6 +114,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 2c325badf..303622f9d 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 2f558ffea..39972dd0e 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0b53cba80..fa82328ef 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -370,6 +370,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index f279853de..4227ab1a7 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f23cfad71..244f48f4f 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index d3d1e485b..c758d1f2d 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -94,7 +94,6 @@
 
 #include "postgres.h"
 
-#include <ctype.h>
 #include <math.h>
 
 #include "access/brin.h"
@@ -193,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -214,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5759,6 +5762,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6817,6 +6906,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6826,17 +6962,21 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
+	double		correlation = 0;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
+	bool		set_correlation = false;
 	bool		eqQualHere;
-	bool		found_saop;
+	bool		found_array;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	bool		upper_inequal_col;
+	bool		lower_inequal_col;
 	double		num_sa_scans;
+	double		inequalselectivity;
 	ListCell   *lc;
 
 	/*
@@ -6852,16 +6992,21 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
-	found_saop = false;
+	found_array = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
+	upper_inequal_col = false;
+	lower_inequal_col = false;
 	num_sa_scans = 1;
+	inequalselectivity = 1;
 	foreach(lc, path->indexclauses)
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
@@ -6869,13 +7014,103 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		past_first_skipped = false;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
-			eqQualHere = false;
-			indexcol++;
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't come after a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT,
+							new_num_sa_scans;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						Assert(!set_correlation);
+						correlation = btcost_correlation(index, &vardata);
+						set_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (!past_first_skipped)
+				{
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is...
+					 */
+					if (!upper_inequal_col)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lower_inequal_col)
+						ndistinct += 1;
+
+					past_first_skipped = true;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				found_array = true;
+				new_num_sa_scans = num_sa_scans * ndistinct;
+
+				/*
+				 * Stop adding new skip arrays when the would-be new
+				 * num_sa_scans exceeds a third of the number of index pages
+				 */
+				if (ceil(index->pages * 0.3333333) < new_num_sa_scans)
+					break;
+
+				/* Done costing skipping for this index column */
+				num_sa_scans = new_num_sa_scans;
+				indexcol++;
+			}
+
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+				break;			/* no quals for indexcol (can't skip scan) */
+
+			/* reset for next indexcol */
+			eqQualHere = false;
+			upper_inequal_col = false;
+			lower_inequal_col = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6897,6 +7132,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6905,7 +7141,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				double		alength = estimate_array_length(root, other_operand);
 
 				clause_op = saop->opno;
-				found_saop = true;
+				found_array = true;
 				/* estimate SA descents by indexBoundQuals only */
 				if (alength > 1)
 					num_sa_scans *= alength;
@@ -6917,7 +7153,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skip scan purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6933,6 +7169,34 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct.
+					 *
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (!upper_inequal_col)
+							inequalselectivity =
+								Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+									DEFAULT_RANGE_INEQ_SEL);
+						upper_inequal_col = true;
+					}
+					else
+					{
+						if (!lower_inequal_col)
+							inequalselectivity =
+								Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+									DEFAULT_RANGE_INEQ_SEL);
+						lower_inequal_col = true;
+					}
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6948,7 +7212,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
-		!found_saop &&
+		!found_array &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
 	else
@@ -6988,14 +7252,18 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * of leaf pages (we make it 1/3 the total number of pages instead) to
 		 * give the btree code credit for its ability to continue on the leaf
 		 * level with low selectivity scans.
+		 *
+		 * Note: num_sa_scans is derived in a way that treats any skip scan
+		 * quals as if they were ScalarArrayOpExpr quals whose array has as
+		 * many distinct elements as possibly-matching values in the index.
 		 */
 		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
 		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
-		 * to integer.
+		 * ScalarArrayOpExpr quals (as well as any skip scan quals) included
+		 * in indexBoundQuals, and then round to integer.
 		 *
 		 * It is tempting to make genericcostestimate behave as if SAOP
 		 * clauses work in almost the same way as scalar operators during
@@ -7056,104 +7324,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!set_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..2bd35d2d2
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns skip support struct, allocating in caller's memory
+ * context.  Otherwise returns NULL, indicating that operator class has no
+ * skip support function.
+ */
+SkipSupport
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse)
+{
+	Oid			skipSupportFunction;
+	SkipSupport sksup;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return NULL;
+
+	sksup = palloc(sizeof(SkipSupportData));
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return sksup;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index ba9bae050..f64fbf263 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2286,6 +2287,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 4f8402ef9..1b72059d2 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,6 +13,7 @@
 
 #include "postgres.h"
 
+#include <limits.h>
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
@@ -21,6 +22,7 @@
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -416,6 +418,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index dc7d14b60..3116c6350 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -825,7 +825,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 053619624..7e23a7b6e 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 8011c141b..1dde8110c 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2248,7 +2248,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2268,7 +2268,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index e6f7b9013..24523f0df 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5325,9 +5325,10 @@ Function              | in_range(time without time zone,time without time zone,i
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index 068c66b95..622e55f72 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -858,7 +858,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -868,7 +868,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a2644a2e6..a8550099a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -219,6 +219,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2684,6 +2685,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.47.2

v22-0004-Convert-nbtree-inequalities-using-skip-support.patchapplication/octet-stream; name=v22-0004-Convert-nbtree-inequalities-using-skip-support.patchDownload
From 6875600c37a936e0852b8ab74fd53856419e7764 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 13 Jan 2025 16:08:32 -0500
Subject: [PATCH v22 4/5] Convert nbtree inequalities using skip support.

Convert the low_compare and high_compare inequalities used by nbtree
skip arrays with skip support: convert a qual such as "WHERE a > 4" into
"WHERE a >= 5", and convert "WHERE a < 888" into "WHERE a <= 887".  This
happens at the end of nbtree preprocessing with skip arrays, after each
skip array has had selected its final low_compare and high_compare.

There is a performance benefit from performing these conversions:
_bt_first will be able to use any lower-order scan keys (there must be
at least one) for the purposes of building an initial positioning
insertion type scan key.
---
 src/backend/access/nbtree/nbtpreprocesskeys.c | 173 ++++++++++++++++++
 1 file changed, 173 insertions(+)

diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index a9f6d896c..0838ba964 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -50,6 +50,12 @@ static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
 								 BTArrayKeyInfo *array, bool *qual_ok);
 static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
 								 BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+									   BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
@@ -1284,6 +1290,164 @@ _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
 	return true;
 }
 
+/*
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts the
+ * operator strategies).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value MINMAXVAL, using >= instead of using >
+ * (or using <= instead of < during backwards scans) makes it safe to include
+ * lower-order scan keys in the insertion scan key (there must be lower-order
+ * scan keys after the skip array).  We will avoid an extra _bt_first to find
+ * the first value in the index > sk_argument, at least when the first real
+ * matching value in the index happens to be an exact match for the newly
+ * transformed (newly incremented/decremented) sk_argument value.
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01'.
+ */
+static void
+_bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+						   BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	/*
+	 * Called last among all preprocessing steps, when the skip array's final
+	 * low_compare and high_compare have both been chosen
+	 */
+	Assert(so->skipScan);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1 && !array->null_elem && array->sksup);
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skiparray_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skiparray_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1820,6 +1984,15 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && array->sksup &&
+						!array->null_elem)
+						_bt_skiparray_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
-- 
2.47.2

#60Heikki Linnakangas
hlinnaka@iki.fi
In reply to: Peter Geoghegan (#57)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 03/01/2025 21:43, Peter Geoghegan wrote:

The newly revised "skipskip" optimization seems to get the regressions
down to only a 5% - 10% increase in runtime across a wide variety of
unsympathetic cases -- I'm now validating performance against a test
suite based on the adversarial cases presented by Masahiro Ikeda on
this thread. Although I think that I'll end up tuning the "skipskip"
mechanism some more (I may have been too conservative in marginal
cases that actually do benefit from skipping), I deem these
regressions to be acceptable. They're only seen in the most
unsympathetic cases, where an omitted leading column has groupings of
no more than about 50 index tuples, making skipping pretty hopeless.

On my laptop, this is the worst case I could come up with:

create table skiptest as select g / 10 as a, g%10 as b from
generate_series(1, 10000000) g;
vacuum freeze skiptest;
create index on skiptest (a, b);

set enable_seqscan=off; set max_parallel_workers_per_gather=0;

\timing on

After repeating a few times, to warm the cache:

postgres=# select count(*) from skiptest where b=1;
count
---------
1000000
(1 row)

Time: 202.501 ms

And after 'set skipscan_prefix_cols=0':

select count(*) from skiptest where b=1;
count
---------
1000000
(1 row)

Time: 164.762 ms

EXPLAIN ANALYZE confirms that it uses an Index Only scan in both cases.

I knew from the outset that the hardest part of this project would be
avoiding regressions in highly unsympathetic cases. The regressions
that are still there seem very difficult to minimize any further; the
overhead that remains comes from the simple need to maintain the
scan's skip arrays once per page, before leaving the page. Once a scan
decides to apply the "skipskip" optimization, it tends to stick with
it for all future leaf pages -- leaving only the overhead of checking
the high key while advancing the scan's arrays. I've cut just about
all that that I can reasonably cut from the hot code paths that are at
issue with the regressed cases.

Hmm, looking at the code and profile with perf, a lot of code is
executed per tuple. Just function calls, passing arguments, checking
flags etc. I suspect you could shave off some cycles by structuring the
code in a more compiler and branch-prediction friendly way. Or just with
more aggressive inlining. Not sure what exactly to suggest, but the code
of _bt_readpage() and all the subroutines it calls is complicated.

Aside from the performance of those routines, it doesn't feel very
intuitive. _bt_checkkeys() not only checks the current keys, but it can
also read ahead tuples on the same page and update the array keys.

One little thing I noticed by stepping with debugger is that it calls
index_getattr() twice for the same tuple and attribute. First in
_bt_check_compare(), and if that sets *continuescan=false, again in
_bt_tuple_before_array_skeys(). The first index_getattr() call is
certainly pretty expensive because that's where you get the cache miss
on the tuple when scanning. Not sure if the second call matters much,
but it feels like a bad smell.

The comment on _bt_tuple_before_array_skeys() says:

* readpagetup callers must only call here when _bt_check_compare already set
* continuescan=false. We help these callers deal with _bt_check_compare's
* inability to distinguishing between the < and > cases (it uses equality
* operator scan keys, whereas we use 3-way ORDER procs)

That begs the question, why does _bt_check_compare() not call the 3-way
ORDER proc in the first place? That would avoid the overhead of another
call here.

Aside from these micro-optimizations, I wonder about the "look-ahead"
logic in _bt_checkkeys_look_ahead. It feels pretty simplistic. Could you
use Exponential Search
(https://en.wikipedia.org/wiki/Exponential_search) instead of a plain
binary search on the page?

--
Heikki Linnakangas
Neon (https://neon.tech)

In reply to: Heikki Linnakangas (#60)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Jan 24, 2025 at 10:07 PM Heikki Linnakangas <hlinnaka@iki.fi> wrote:

On my laptop, this is the worst case I could come up with:

create table skiptest as select g / 10 as a, g%10 as b from
generate_series(1, 10000000) g;
vacuum freeze skiptest;
create index on skiptest (a, b);

set enable_seqscan=off; set max_parallel_workers_per_gather=0;

\timing on

After repeating a few times, to warm the cache:

postgres=# select count(*) from skiptest where b=1;
count
---------
1000000
(1 row)

I can reproduce this. However, it should be noted that the regression
completely goes away if I make one small change to your test case: all
I need to do is make sure that the CREATE INDEX happens *before*
inserting any rows into the table. Once I do that, suffix truncation
tends to be quite a bit more effective. This makes all the difference
with your test case, since it encourages the existing heuristics
within _bt_advance_array_keys to do the right thing and stick on the
leaf level. That allows the "skipskip" mechanism to kick in as
expected, which doesn't seem to be happening when the index is built
by CREATE INDEX.

Of course, this doesn't make your adversarial case invalid. But it
does suggest a solution: Maybe nbtsort.c could be taught to be more
careful about "picking a split point", matching the behavior of
nbtsplitloc.c. Alternatively, I could invent some new heuristics with
this issue in mind.

I already had an open item in my personal TODO. That open item
concerns backwards scans, which don't use the high key within
_bt_advance_array_keys. As such they cannot really expect to benefit
in the same way by my suggested changes to nbtsort.c.

Your adversarial case is probably exactly the same issue as the
backwards scan issue I plan on looking into, even though you used a
forward scan + CREATE INDEX. So I probably need a solution that'll
work just as well, regardless of how effective suffix truncation is
(since backwards scans will never have a "low key" to consider what's
likely to be on the next page in any case).

Aside from the performance of those routines, it doesn't feel very
intuitive. _bt_checkkeys() not only checks the current keys, but it can
also read ahead tuples on the same page and update the array keys.

True. But none of that is new. That's all from Postgres 17.

One little thing I noticed by stepping with debugger is that it calls
index_getattr() twice for the same tuple and attribute. First in
_bt_check_compare(), and if that sets *continuescan=false, again in
_bt_tuple_before_array_skeys(). The first index_getattr() call is
certainly pretty expensive because that's where you get the cache miss
on the tuple when scanning. Not sure if the second call matters much,
but it feels like a bad smell.

Possibly, but the right thing to do here is to just not maintain the
skip arrays at all. What's at issue with your test case is that the
scan doesn't quite manage to notice that that's what it should do. You
might still be right about this, but it is nevertheless true that this
*shouldn't* be relevant (it is relevant right now, but it's not hard
to see that it doesn't have to be relevant).

The comment on _bt_tuple_before_array_skeys() says:

* readpagetup callers must only call here when _bt_check_compare already set
* continuescan=false. We help these callers deal with _bt_check_compare's
* inability to distinguishing between the < and > cases (it uses equality
* operator scan keys, whereas we use 3-way ORDER procs)

That begs the question, why does _bt_check_compare() not call the 3-way
ORDER proc in the first place? That would avoid the overhead of another
call here.

Again, this is a design decision made by the Postgres 17 SAOP patch.

I think that there is something to be said for matching the behavior
of regular scans, including using operators (not 3-way ORDER procs)
when scanning on the leaf level.

Aside from these micro-optimizations, I wonder about the "look-ahead"
logic in _bt_checkkeys_look_ahead. It feels pretty simplistic. Could you
use Exponential Search
(https://en.wikipedia.org/wiki/Exponential_search) instead of a plain
binary search on the page?

Maybe. But, again, I don't think that that's really the issue with
your test case. The issue is that it doesn't quite manage to use the
skipskip optimization, even though that's clearly possible, and
actually fixes the issue. Once it does that then
_bt_checkkeys_look_ahead won't ever be used, so it can't matter (at
least not as far as the query you came up with is concerned).

Let me get back to you on this.

Thanks for the review!

--
Peter Geoghegan

In reply to: Peter Geoghegan (#61)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Jan 24, 2025 at 10:38 PM Peter Geoghegan <pg@bowt.ie> wrote:

I can reproduce this. However, it should be noted that the regression
completely goes away if I make one small change to your test case: all
I need to do is make sure that the CREATE INDEX happens *before*
inserting any rows into the table. Once I do that, suffix truncation
tends to be quite a bit more effective. This makes all the difference
with your test case, since it encourages the existing heuristics
within _bt_advance_array_keys to do the right thing and stick on the
leaf level. That allows the "skipskip" mechanism to kick in as
expected, which doesn't seem to be happening when the index is built
by CREATE INDEX.

I think that I could have done better at explaining myself here. I'll
have another go at that:

Your test case showed an excessive number of primitive index scans:
EXPLAIN ANALYZE showed "Index Searches: 21859", even though the ideal
number of index searches is 1, given all these specifics. It would be
understandable if a person saw that and concluded that the added
overhead/regression comes from descending the index many more times
than is truly necessary -- blaming the added overhead that comes from
all those extra _bt_search calls is a good guess. But that's not it.
Not really.

You (Heikki) didn't actually make any claims about _bt_search being
too hot during profiling of this test case. You didn't actually see
_bt_search show up prominently when you ran "perf". What you actually
saw (and actually complained about) was stuff that is called from
_bt_readpage, to deal with array maintenance. Even still, I want to
avoid making this any more confusing than it needs to be. The nature
of the problem needs to be carefully teased apart.

There is an important sense in which the issue of excessive primitive
index scans *is* related to the regression from wasting cycles on
array maintenance, though only indirectly: the heuristics that decide
if the skipskip mechanism should be enabled during a _bt_readpage call
are indirectly influenced by certain other heuristics, in
_bt_advance_array_keys.
These other heuristics are the heuristics that determine
whether or not we'll start another primitive index scan (all of which
are in _bt_advance_array_key, and were added in Postgres 17, and
haven't been touched by this patch series).

More concretely: if we choose to start another primitive index scan,
and make a bad choice (because we land on the very next sibling leaf
page when we could gotten to by simply stepping right without calling
_bt_first/_bt_search again), then we also won't have an opportunity to
apply the skipskip mechanism when on that same sibling leaf page.
That's because in practice every leaf page read within _bt_readpage
will be the first leaf page of the ongoing primitive index scan with
this test case. Being the first leaf page of a primscan supposedly
makes a leaf page a bad target for the skipskip optimization, and so
we never actually apply the skipskip optimization in practice here.

Again, the real problem is simply that we're not applying the skipskip
optimization at all -- even though it was specifically written with
cases like Heikki's adversarial case in mind, and even though it
actually works as designed once it is forced to activate with Heikki's
test case. It may also be a bit of a problem that there's 21859 calls to
_bt_search instead of just 1, but that's a surprisingly small
contributor to the added overhead. (I'll probably fix the problem by
avoiding useless primitive index scans in the first place, rather than
by changing the heuristics that activate skipskip, which condition the
use of skipskip on firstPage=false. But, again, that doesn't mean that
the problem is excessive primscan overhead from all of the extra
_bt_first/_bt_search calls. The problem is indirect, and so my solution
can be indirect, too.)

--
Peter Geoghegan

In reply to: Peter Geoghegan (#61)
5 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Jan 24, 2025 at 10:38 PM Peter Geoghegan <pg@bowt.ie> wrote:

On Fri, Jan 24, 2025 at 10:07 PM Heikki Linnakangas <hlinnaka@iki.fi> wrote:

On my laptop, this is the worst case I could come up with:

create table skiptest as select g / 10 as a, g%10 as b from
generate_series(1, 10000000) g;
vacuum freeze skiptest;
create index on skiptest (a, b);

set enable_seqscan=off; set max_parallel_workers_per_gather=0;

\timing on

After repeating a few times, to warm the cache:

postgres=# select count(*) from skiptest where b=1;
count
---------
1000000
(1 row)

Your adversarial case is probably exactly the same issue as the
backwards scan issue I plan on looking into, even though you used a
forward scan + CREATE INDEX. So I probably need a solution that'll
work just as well, regardless of how effective suffix truncation is
(since backwards scans will never have a "low key" to consider what's
likely to be on the next page in any case).

Attached is v23, which fixes this issue by making sure that the
"skipskip" optimization is actually applied -- something that was
always *supposed* to happen in cases such as this one. With v22, your
adversarial case was a little over 20% slower. With this v23 I have
the regression down to under 3%, since we'll now apply the skipskip
optimization as expected. This level of slowdown seems acceptable to
me, for a case such as this (after all, this is an index scan that the
optimizer is unlikely to ever actually choose).

The way that the "skipskip" optimization works is unchanged in v23.
And even the way that we decide whether to apply that optimization
didn't really change, either. What's new in v23 is that
v23-0003-*patch adds rules around primitive scan scheduling.
Obviously, I specifically targeted Heikki's regression when I came up
with this, but the new _bt_advance_array_keys rules are nevertheless
orthogonal: even scans that just use conventional SAOP arrays will
also use these new _bt_advance_array_keys heuristics (though it'll
tend to matter much less there).

As I attempted to explain recently (admittedly it's quite confusing),
making better choices around scheduling primitive index scans is
important for its own sake, for fairly obvious reasons, but it's even
more important for far more subtle reasons: better scheduling gives
the skipskip heuristics a clearer picture of what's really going on.
That's why more or less the same set of skipskip heuristics now work
much better in practice (in cases such as Heikki's adversarial case).
It's rather indirect.

Again, the problem that Heikki highlighted was more or less an
unforeseen, far removed consequence of not having a very clear picture
about how to schedule primitive index scans within
_bt_advance_array_keys. I proved this when I showed that Heikki's
problem went away, even with v22, once retail inserts were used
instead of CREATE INDEX. I demonstrated that that'll allow the
nbtsplitloc.c suffix truncation stuff to work, which is one way to
give the _bt_advance_array_keys primitive scan scheduling a clearer
picture of what it should be doing. With v23 I came up with another
way of doing that -- a way that actually works with indexes built with
CREATE INDEX (as well as with backwards scans, which had essentially
the same problem even when suffix truncation was working well).

The structure of the existing code changes somewhat to accommodate the
new requirements: v23 moves the scanBehind recheck process from
_bt_checkkeys into its _bt_readpage caller. This was arguably already
a missed opportunity for my commit 79fa7b3b from late last year. If we
expect _bt_readpage to explicitly check a flag to perform a
so->scanBehind recheck in one case, then we might as well have it do
so in all cases. It makes what's really going with on with the scan
quite a bit clearer.

--
Peter Geoghegan

Attachments:

v23-0002-Add-nbtree-skip-scan-optimizations.patchapplication/octet-stream; name=v23-0002-Add-nbtree-skip-scan-optimizations.patchDownload
From 78dc496c012e85b4442d02a6d9e68089470c310b Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v23 2/5] Add nbtree skip scan optimizations.

Teach nbtree composite index scans to opportunistically skip over
irrelevant sections of the index.  This is possible whenever the scan
encounters a group of tuples that all share a common prefix of the same
index column value.  Each such group of tuples can be thought of as a
"logical subindex".  The scan exhaustively "searches every subindex".
Scans of composite indexes with a leading low cardinality column can now
skip over irrelevant sections of the index, which is far more efficient.

When nbtree is passed input scan keys derived from a query predicate
"WHERE b = 5", new nbtree preprocessing steps now output scan keys for
"WHERE a = ANY(<every possible 'a' value>) AND b = 5".  That is, nbtree
preprocessing generates a "skip array" (and an associated scan key) for
the omitted column "a", thereby enabling marking the scan key on "b" as
required to continue the scan.  This approach builds on the design for
ScalarArrayOp scans established by commit 5bf748b8: skip arrays generate
their values procedurally, but otherwise work just like SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values behave just like any other
value that might appear in an array -- though they can never actually
locate matching index tuples.

Inequality scan keys can affect how skip arrays generate their values.
Their range is constrained by the inequalities.  For example, a skip
array on "a" will only ever use element values 1 and 2 given a qual
"WHERE a BETWEEN 1 AND 2 AND b = 66".  A scan using such a skip array
has almost identical performance characteristics to a scan with the qual
"WHERE a = ANY('{1, 2}') AND b = 66".  The scan will be much faster when
it can be executed as two highly selective primitive index scans, rather
than a single much larger scan that has to read many more leaf pages.
It isn't guaranteed to improve performance, though.

B-Tree preprocessing is optimistic about skipping working out: it
applies simple generic rules to determine where to generate skip arrays.
This assumes that the runtime overhead of maintaining skip arrays will
pay for itself, or at worst lead to only a modest loss in performance.
As things stand, our assumptions about possible downsides are much too
optimistic: skip array maintenance will lead to regressions that are
clearly unacceptable with unsympathetic queries (typically queries where
the fastest plan necessitates a traditional full index scan, with little
to no potential for the scan to skip over irrelevant index leaf pages).
A pending commit will ameliorate the problems in this area by making
affected scans temporarily disable skip array maintenance for a page.
It seems natural to commit this separately, since the break-even point
at which the scan should favor skipping (or favor sequentially reading
every index tuple, without the use of any extra skip array keys) is the
single most subtle aspect of the work in this area.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |   3 +-
 src/include/access/nbtree.h                   |  35 +-
 src/include/catalog/pg_amproc.dat             |  22 +
 src/include/catalog/pg_proc.dat               |  27 +
 src/include/storage/lwlock.h                  |   1 +
 src/include/utils/skipsupport.h               |  97 +++
 src/backend/access/index/indexam.c            |   3 +-
 src/backend/access/nbtree/nbtcompare.c        | 273 +++++++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 599 +++++++++++++--
 src/backend/access/nbtree/nbtree.c            | 211 +++++-
 src/backend/access/nbtree/nbtsearch.c         | 116 ++-
 src/backend/access/nbtree/nbtutils.c          | 713 ++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |   4 +
 src/backend/commands/opclasscmds.c            |  25 +
 src/backend/storage/lmgr/lwlock.c             |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/date.c                  |  46 ++
 src/backend/utils/adt/meson.build             |   1 +
 src/backend/utils/adt/selfuncs.c              | 405 +++++++---
 src/backend/utils/adt/skipsupport.c           |  61 ++
 src/backend/utils/adt/timestamp.c             |  48 ++
 src/backend/utils/adt/uuid.c                  |  70 ++
 doc/src/sgml/btree.sgml                       |  34 +-
 doc/src/sgml/indexam.sgml                     |   3 +-
 doc/src/sgml/indices.sgml                     |  49 +-
 doc/src/sgml/xindex.sgml                      |  16 +-
 src/test/regress/expected/alter_generic.out   |  10 +-
 src/test/regress/expected/btree_index.out     |  41 +
 src/test/regress/expected/create_index.out    | 103 ++-
 src/test/regress/expected/psql.out            |   3 +-
 src/test/regress/sql/alter_generic.sql        |   5 +-
 src/test/regress/sql/btree_index.sql          |  21 +
 src/test/regress/sql/create_index.sql         |  51 +-
 src/tools/pgindent/typedefs.list              |   3 +
 35 files changed, 2765 insertions(+), 337 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index 6723de75a..a718b864f 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -214,7 +214,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 000c7289b..346ee92a8 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -707,6 +708,10 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
  *	(BTOPTIONS_PROC).  These procedures define a set of user-visible
  *	parameters that can be used to control operator class behavior.  None of
  *	the built-in B-Tree operator classes currently register an "options" proc.
+ *
+ *	To facilitate more efficient B-Tree skip scans, an operator class may
+ *	choose to offer a sixth amproc procedure (BTSKIPSUPPORT_PROC).  For full
+ *	details, see src/include/utils/skipsupport.h.
  */
 
 #define BTORDER_PROC		1
@@ -714,7 +719,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1027,10 +1033,21 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
-	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+	int			cur_elem;		/* index of current element in elem_values */
+
+	/* fields for skip arrays, which generate element datums procedurally */
+	int16		attlen;			/* attr len in bytes */
+	bool		attbyval;		/* as FormData_pg_attribute.attbyval */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupport sksup;			/* skip support (NULL if opclass lacks it) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1042,6 +1059,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Last array advancement matched -inf attr? */
 	bool		oppositeDirCheck;	/* explicit scanBehind recheck needed? */
@@ -1118,6 +1136,15 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on column without input = */
+
+/* SK_BT_SKIP-only flags (mutable, set and unset by array advancement) */
+#define SK_BT_MINVAL	0x00080000	/* invalid sk_argument, use low_compare */
+#define SK_BT_MAXVAL	0x00100000	/* invalid sk_argument, use high_compare */
+#define SK_BT_NEXT		0x00200000	/* positions the scan > sk_argument */
+#define SK_BT_PRIOR		0x00400000	/* positions the scan < sk_argument */
+
+/* Remaps pg_index flag bits to uppermost SK_BT_* byte */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1164,7 +1191,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 508f48d34..e9194e68e 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5b8c2ad2a..5f3cb5619 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2260,6 +2275,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4450,6 +4468,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6322,6 +6343,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9370,6 +9394,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 2aa46fd50..6803d5b9e 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..94389ead2
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,97 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining groups of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code falls back on next-key sentinel values for any opclass that
+ * doesn't provide its own skip support function.  There is no benefit to
+ * providing skip support for an opclass on a type where guessing that the
+ * next indexed value is the next possible indexable value seldom works out.
+ * It might not even be feasible to add skip support for a continuous type.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order)
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 * Operator class decrement/increment functions will never be called with
+	 * a NULL "existing" argument, either.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern SkipSupport PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+												 bool reverse);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 8b1f55543..0e62d7a7a 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 291cb8fc1..4da5a3c1d 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 1fd1da5f1..2dd4b592c 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -45,8 +45,15 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
+static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
+								 ScanKey skey, FmgrInfo *orderproc,
+								 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
+								 BTArrayKeyInfo *array, bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
+							   int *numSkipArrayKeys);
 static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
 									  Oid elemtype, StrategyNumber strat,
 									  Datum *elems, int nelems);
@@ -89,6 +96,8 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -101,10 +110,16 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never generated skip array scan keys, it would be possible for "gaps"
+ * to appear that make it unsafe to mark any subsequent input scan keys
+ * (copied from scan->keyData[]) as required to continue the scan.  Prior to
+ * Postgres 18, a qual like "WHERE y = 4" always required a full index scan.
+ * It'll now become "WHERE x = ANY('{every possible x value}') and y = 4" on
+ * output, due to the addition of a skip array on "x".  This has the potential
+ * to be much more efficient than a full index scan (though it'll behave like
+ * a full index scan when there are many distinct values for "x").
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -141,7 +156,12 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -200,6 +220,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProcs[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -231,6 +259,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
+		Assert(!so->skipScan);
 
 		return;
 	}
@@ -288,7 +317,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -345,7 +375,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -393,6 +422,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, _bt_preprocess_array_keys has already added "=" keys
+			 * sufficient to form an unbroken series of "=" constraints on all
+			 * attrs prior to the attr from the final scan->keyData[] key.
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -481,6 +515,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -489,6 +524,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -508,8 +544,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -803,6 +837,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -812,6 +849,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -885,6 +938,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -978,24 +1032,55 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
  * Compare an array scan key to a scalar scan key, eliminating contradictory
  * array elements such that the scalar scan key becomes redundant.
  *
+ * If the opfamily is incomplete we may not be able to determine which
+ * elements are contradictory.  When we return true we'll have validly set
+ * *qual_ok, guaranteeing that at least the scalar scan key can be considered
+ * redundant.  We return false if the comparison could not be made (caller
+ * must keep both scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar scan keys.
+ */
+static bool
+_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
+							   bool *qual_ok)
+{
+	Assert(arraysk->sk_attno == skey->sk_attno);
+	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
+		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
+	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
+		   skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Just call the appropriate helper function based on whether it's a SAOP
+	 * array or a skip array.  Both helpers will set *qual_ok in passing.
+	 */
+	if (array->num_elems != -1)
+		return _bt_saoparray_shrink(scan, arraysk, skey, orderproc, array,
+									qual_ok);
+	else
+		return _bt_skiparray_shrink(scan, skey, array, qual_ok);
+}
+
+/*
+ * Preprocessing of SAOP (non-skip) array scan key, used to determine which
+ * array elements are eliminated as contradictory by a non-array scalar key.
+ * _bt_compare_array_scankey_args helper function.
+ *
  * Array elements can be eliminated as contradictory when excluded by some
  * other operator on the same attribute.  For example, with an index scan qual
  * "WHERE a IN (1, 2, 3) AND a < 2", all array elements except the value "1"
  * are eliminated, and the < scan key is eliminated as redundant.  Cases where
  * every array element is eliminated by a redundant scalar scan key have an
  * unsatisfiable qual, which we handle by setting *qual_ok=false for caller.
- *
- * If the opfamily doesn't supply a complete set of cross-type ORDER procs we
- * may not be able to determine which elements are contradictory.  If we have
- * the required ORDER proc then we return true (and validly set *qual_ok),
- * guaranteeing that at least the scalar scan key can be considered redundant.
- * We return false if the comparison could not be made (caller must keep both
- * scan keys when this happens).
  */
 static bool
-_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
-							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
-							   bool *qual_ok)
+_bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+					 FmgrInfo *orderproc, BTArrayKeyInfo *array, bool *qual_ok)
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
@@ -1006,14 +1091,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
-	Assert(arraysk->sk_attno == skey->sk_attno);
 	Assert(array->num_elems > 0);
-	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
-		   arraysk->sk_strategy == BTEqualStrategyNumber);
-	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
-		   skey->sk_strategy != BTEqualStrategyNumber);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
 
 	/*
 	 * _bt_binsrch_array_skey searches an array for the entry best matching a
@@ -1112,6 +1191,101 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	return true;
 }
 
+/*
+ * Preprocessing of skip (non-SAOP) array scan key, used to determine
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function.
+ *
+ * Unlike _bt_saoparray_shrink, we don't modify caller's array in-place.  Skip
+ * arrays work by procedurally generating their elements as needed, so we just
+ * store the inequality as the skip array's low_compare or high_compare.  The
+ * array's elements will be generated from the range of values that satisfies
+ * both low_compare and high_compare.
+ */
+static bool
+_bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
+					 bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Array's index attribute will be constrained by a strict operator/key.
+	 * Array must not "contain a NULL element" (i.e. the scan must not apply
+	 * "IS NULL" qual when it reaches the end of the index that stores NULLs).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Consider if we should treat caller's scalar scan key as the skip
+	 * array's high_compare or low_compare.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way the scan can always safely assume that
+	 * it's okay to use the input-opclass-only-type proc from so->orderProcs[]
+	 * (they can be cross-type with a SAOP array, but never with skip arrays).
+	 *
+	 * This approach is enabled by MINVAL/MAXVAL sentinel key markings, which
+	 * can be thought of as representing either the lowest or highest matching
+	 * array element.  An = array key marked MINVAL/MAXVAL never has a valid
+	 * datum stored in its sk_argument.  The scan must directly apply the
+	 * array's low_compare when it encounters MINVAL (or its high_compare when
+	 * it encounters MAXVAL), and must never use array's so->orderProcs[] proc
+	 * against low_compare's/high_compare's sk_argument.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* try to keep only one high_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* discard new high_compare, keep old one */
+
+				/* replace old high_compare with caller's new one */
+			}
+
+			/* caller's high_compare becomes skip array's high_compare */
+			array->high_compare = skey;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* try to keep only one low_compare inequality */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* discard new low_compare, keep old one */
+
+				/* replace old low_compare with caller's new one */
+			}
+
+			/* caller's low_compare becomes skip array's low_compare */
+			array->low_compare = skey;
+			break;
+		case BTEqualStrategyNumber:
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1137,6 +1311,12 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -1152,41 +1332,36 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	Oid			skip_eq_ops[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
-	ScanKey		cur;
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
+	so->skipScan = (numSkipArrayKeys > 0);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys we'll need to generate below
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -1201,18 +1376,20 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
+		ScanKey		inkey = scan->keyData + input_ikey,
+					cur;
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
 		Oid			elemtype;
@@ -1225,21 +1402,114 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		Datum	   *elem_values;
 		bool	   *elem_nulls;
 		int			num_nonnulls;
-		int			j;
+
+		/* set up next output scan key */
+		cur = &arrayKeyData[numArrayKeyData];
+
+		/* Backfill skip arrays for attrs < or <= input key's attr? */
+		while (numSkipArrayKeys && attno_skip <= inkey->sk_attno)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skip_eq_ops[attno_skip - 1];
+			CompactAttribute *attr;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/*
+				 * Attribute already has an = input key, so don't output a
+				 * skip array for attno_skip.  Just copy attribute's = input
+				 * key into arrayKeyData[] once outside this inner loop.
+				 *
+				 * Note: When we get here there must be an additional index
+				 * attribute that lacks an equality input key, and still needs
+				 * a skip array (numSkipArrayKeys would be 0 if there wasn't).
+				 */
+				Assert(attno_skip == inkey->sk_attno);
+				/* inkey can't be last input key to be marked required: */
+				Assert(input_ikey < scan->numberOfKeys - 1);
+#if 0
+				/* Could be a redundant input scan key, so can't do this: */
+				Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+					   (inkey->sk_flags & SK_SEARCHNULL));
+#endif
+
+				attno_skip++;
+				break;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize generic BTArrayKeyInfo fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+
+			/* Initialize skip array specific BTArrayKeyInfo fields */
+			attr = TupleDescCompactAttr(RelationGetDescr(rel), attno_skip - 1);
+			reverse = (indoption[attno_skip - 1] & INDOPTION_DESC) != 0;
+			so->arrayKeys[numArrayKeys].attlen = attr->attlen;
+			so->arrayKeys[numArrayKeys].attbyval = attr->attbyval;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup =
+				PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse);
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * We'll need a 3-way ORDER proc to perform "binary searches" for
+			 * the next matching array element.  Set that up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			numArrayKeys++;
+			numArrayKeyData++;	/* keep this scan key/array */
+
+			/* set up next output scan key */
+			cur = &arrayKeyData[numArrayKeyData];
+
+			/* remember having output this skip array and scan key */
+			numSkipArrayKeys--;
+			attno_skip++;
+		}
 
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
-		*cur = scan->keyData[input_ikey];
+		*cur = *inkey;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -1257,7 +1527,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * all btree operators are strict.
 		 */
 		num_nonnulls = 0;
-		for (j = 0; j < num_elems; j++)
+		for (int j = 0; j < num_elems; j++)
 		{
 			if (!elem_nulls[j])
 				elem_values[num_nonnulls++] = elem_values[j];
@@ -1295,7 +1565,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -1306,7 +1576,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -1323,7 +1593,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -1392,23 +1662,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			origelemtype = elemtype;
 		}
 
-		/*
-		 * And set up the BTArrayKeyInfo data.
-		 *
-		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
-		 * scan_key field later on, after so->keyData[] has been finalized.
-		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		/* Initialize generic BTArrayKeyInfo fields */
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
+
+		/* Initialize SAOP array specific BTArrayKeyInfo fields */
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].cur_elem = -1;	/* i.e. invalid */
+
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
+	Assert(numSkipArrayKeys == 0);
+
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -1514,7 +1785,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -1565,7 +1837,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -1575,6 +1847,189 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit every so->arrayKeys[] entry.
+ *
+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller must add this many
+ * skip arrays to each of the most significant attributes lacking any keys
+ * that use the = strategy (IS NULL keys count as = keys here).  The specific
+ * attributes that need skip arrays are indicated by initializing caller's
+ * skip_eq_ops[] 0-based attribute offset to a valid = op strategy Oid.  We'll
+ * only ever set skip_eq_ops[] entries to InvalidOid for attributes that
+ * already have an equality key in scan->keyData[] input keys -- and only when
+ * there's some later "attribute gap" for us to "fill-in" with a skip array.
+ *
+ * We're optimistic about skipping working out: we always add exactly the skip
+ * arrays needed to maximize the number of input scan keys that can ultimately
+ * be marked as required to continue the scan (but no more).  For a composite
+ * index on (a, b, c, d), we'll instruct caller to add skip arrays as follows:
+ *
+ * Input keys						Output keys (after all preprocessing)
+ * ----------						-------------------------------------
+ * a = 1							a = 1 (no skip arrays)
+ * a = ANY('{0, 1}')				a = ANY('{0, 1}') (no skip arrays)
+ * b = 42							skip a AND b = 42
+ * b = ANY('{40, 42}')				skip a AND b = ANY('{40, 42}')
+ * a = 1 AND b = 42					a = 1 AND b = 42 (no skip arrays)
+ * a >= 1 AND b = 42				range skip a AND b = 42
+ * a = 1 AND b > 42					a = 1 AND b > 42 (no skip arrays)
+ * a >= 1 AND a <= 3 AND b = 42		range skip a AND b = 42
+ * a = 1 AND c <= 27				a = 1 AND skip b AND c <= 27
+ * a = 1 AND d >= 1					a = 1 AND skip b AND skip c AND d >= 1
+ * a = 1 AND b >= 42 AND d > 1		a = 1 AND range skip b AND skip c AND d > 1
+ */
+static int
+_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_skip = 1,
+				attno_inkey = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+#ifdef DEBUG_DISABLE_SKIP_SCAN
+	/* don't attempt to add skip arrays */
+	return numArrayKeys;
+#endif
+
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+			/* Look up input opclass's equality operator (might fail) */
+			skip_eq_ops[attno_skip - 1] =
+				get_opfamily_member(opfamily, opcintype, opcintype,
+									BTEqualStrategyNumber);
+			if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key
+		 * (doesn't matter whether that attribute has an equality key, or just
+		 * uses inequality keys).
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys
+			 */
+			if (attno_has_equal)
+			{
+				/* Attributes with an = key must have InvalidOid eq_op set */
+				skip_eq_ops[attno_skip - 1] = InvalidOid;
+			}
+			else
+			{
+				Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+				Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+				/* Look up input opclass's equality operator (might fail) */
+				skip_eq_ops[attno_skip - 1] =
+					get_opfamily_member(opfamily, opcintype, opcintype,
+										BTEqualStrategyNumber);
+
+				if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+				{
+					/*
+					 * Input opclass lacks an equality strategy operator, so
+					 * don't generate a skip array that definitely won't work
+					 */
+					break;
+				}
+
+				/* plan on adding a backfill skip array for this attribute */
+				(*numSkipArrayKeys)++;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required later on.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
 /*
  * _bt_find_extreme_element() -- get least or greatest array element
  *
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index efcbf8828..c44d07e7d 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -30,6 +30,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -71,7 +72,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -79,11 +80,24 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 *
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -412,6 +426,7 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 		memcpy(scan->keyData, scankey, scan->numberOfKeys * sizeof(ScanKeyData));
 	so->numberOfKeys = 0;		/* until _bt_preprocess_keys sets it */
 	so->numArrayKeys = 0;		/* ditto */
+	so->skipScan = false;		/* tracks if skip arrays are in use */
 }
 
 /*
@@ -538,10 +553,155 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume all input scankeys will be output with its own
+	 * SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * scan array (and associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		CompactAttribute *attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescCompactAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array/skip scan key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   array->attbyval, array->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array/skip scan key, while freeing memory allocated
+		 * for old sk_argument where required
+		 */
+		if (!array->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -552,7 +712,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -575,16 +736,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -611,6 +772,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -655,7 +817,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -677,14 +839,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -722,7 +879,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -766,11 +923,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -809,7 +966,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -818,7 +975,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -836,6 +993,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -845,7 +1003,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -855,14 +1013,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 941b4eaaf..cdbfd5a13 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -964,6 +964,15 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  All
+	 * array keys are always considered = keys, but we'll sometimes need to
+	 * treat the current key value as if we were using an inequality strategy.
+	 * This happens whenever a skip array's scan key is found to have been set
+	 * to one of the special sentinel values representing an imaginary point
+	 * before/after some (or all) of the index's real indexable values.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1039,8 +1048,56 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is MINVAL, choose low_compare (when scanning
+				 * backwards it'll be MAXVAL, and we'll choose high_compare).
+				 *
+				 * Note: if the array's low_compare key makes 'chosen' NULL,
+				 * then we behave as if the array's first element is -inf,
+				 * except when !array->null_elem, which implies a usable NOT
+				 * NULL constraint (or an explicit IS NOT NULL input key).
+				 */
+				if (chosen != NULL &&
+					(chosen->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skipequalitykey = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					Assert(so->skipScan);
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MAXVAL));
+						chosen = array->low_compare;
+					}
+					else
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MINVAL));
+						chosen = array->high_compare;
+					}
+
+					Assert(chosen == NULL ||
+						   chosen->sk_attno == skipequalitykey->sk_attno);
+
+					if (!array->null_elem)
+						impliesNN = skipequalitykey;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1083,9 +1140,46 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 
 				/*
-				 * Done if that was the last attribute, or if next key is not
-				 * in sequence (implying no boundary key is available for the
-				 * next attribute).
+				 * If the key that we just added to startKeys[] is a skip
+				 * array = key whose current element is marked NEXT or PRIOR,
+				 * make strat_total > or < (and stop adding boundary keys).
+				 * This only happens when input opclass lacks skip support.
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(so->skipScan);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(strat_total == BTEqualStrategyNumber);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no sense in
+					 * trying to save later boundary keys in startKeys[].
+					 *
+					 * Note: 'chosen' could be marked SK_ISNULL, in which case
+					 * startKeys[] will position us at first tuple ">" NULL
+					 * (for backwards scans it'll position us at _last_ tuple
+					 * "<" NULL instead).
+					 */
+					break;
+				}
+
+				/*
+				 * Done if that was the last index attribute.  Also done if
+				 * there is a gap index attribute that lacks any usable keys
+				 * (only possible in edge cases where preprocessing could not
+				 * generate a skip array key that "fills the gap").
 				 */
 				if (i >= so->numberOfKeys ||
 					cur->sk_attno != curattr + 1)
@@ -1576,10 +1670,12 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * corresponding value from the last item on the page.  So checking with
 	 * the last item on the page would give a more precise answer.
 	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
+	 * We don't do this for the first page read by each (primitive) scan, to
+	 * avoid slowing down point queries.  They typically don't stand to gain
+	 * much when the optimization can be applied, and are more likely to
+	 * notice the overhead of the precheck.  Also avoid it during skip scans,
+	 * where individual primitive scans that don't just read one leaf page are
+	 * likely to advance their skip array(s) several times per leaf page read.
 	 *
 	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
 	 * just set a low-order required array's key to the best available match
@@ -1603,7 +1699,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!firstPage && !so->scanBehind && minoff < maxoff)
+	if (!firstPage && !so->skipScan && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 693e43c67..c46745e9b 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -30,6 +30,17 @@
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									  int32 set_elem_result, Datum tupdatum, bool tupnull);
+static void _bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_array_set_low_or_high(Relation rel, ScanKey skey,
+									  BTArrayKeyInfo *array, bool low_not_high);
+static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -205,6 +216,7 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 	int32		result = 0;
 
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
+	Assert(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)));
 
 	if (tupnull)				/* NULL tupdatum */
 	{
@@ -281,6 +293,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -403,6 +417,164 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, since skip arrays don't really
+ * contain elements (they generate their array elements procedurally instead).
+ * Our interface matches that of _bt_binsrch_array_skey in every other way.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  We return -1 when tupdatum/tupnull is lower that any value
+ * within the range of the array, and 1 when it is higher than every value.
+ * Caller should pass *set_elem_result to _bt_skiparray_set_element to advance
+ * the array.
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (array->null_elem)
+	{
+		Assert(!array->low_compare && !array->high_compare);
+
+		*set_elem_result = 0;
+		return;
+	}
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
+/*
+ * _bt_skiparray_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Caller passes set_elem_result returned by _bt_binsrch_skiparray_skey for
+ * caller's tupdatum/tupnull.
+ *
+ * We copy tupdatum/tupnull into skey's sk_argument iff set_elem_result == 0.
+ * Otherwise, we set skey to either the lowest or highest value that's within
+ * the range of caller's skip array (whichever is the best available match to
+ * tupdatum/tupnull that is still within the range of the skip array according
+ * to _bt_binsrch_skiparray_skey/set_elem_result).
+ */
+static void
+_bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  int32 set_elem_result, Datum tupdatum, bool tupnull)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (set_elem_result)
+	{
+		/* tupdatum/tupnull is out of the range of the skip array */
+		Assert(!array->null_elem);
+
+		_bt_array_set_low_or_high(rel, skey, array, set_elem_result < 0);
+		return;
+	}
+
+	/* Advance skip array to tupdatum (or tupnull) value */
+	if (unlikely(tupnull))
+	{
+		_bt_skiparray_set_isnull(rel, skey, array);
+		return;
+	}
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen);
+}
+
+/*
+ * _bt_skiparray_set_isnull() -- set skip array scan key to NULL
+ */
+static void
+_bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->null_elem && !array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -412,29 +584,349 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_array_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_array_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  bool low_not_high)
+{
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting array to lowest element (according to low_compare) */
+		skey->sk_flags |= SK_BT_MINVAL;
+	}
+	else
+	{
+		/* Setting array to highest element (according to high_compare) */
+		skey->sk_flags |= SK_BT_MAXVAL;
+	}
+}
+
+/*
+ * _bt_array_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the minimum value within the range
+	 * of a skip array (often just -inf) is never decrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly decrement sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Decrement" from NULL to the high_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->high_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup->decrement(rel, skey->sk_argument, &uflow);
+	if (unlikely(uflow))
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_skiparray_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_array_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MINVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the maximum value within the range
+	 * of a skip array (often just +inf) is never incrementable
+	 */
+	if (skey->sk_flags & SK_BT_MAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly increment sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Increment" from NULL to the low_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->low_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup->increment(rel, skey->sk_argument, &oflow);
+	if (unlikely(oflow))
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_skiparray_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -450,6 +942,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -459,29 +952,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_array_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_array_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -541,6 +1035,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -548,7 +1043,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -560,16 +1054,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_array_set_low_or_high(rel, cur, array,
+								  ScanDirectionIsForward(dir));
 	}
 }
 
@@ -694,9 +1182,70 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))
+		{
+			/* Scankey has a valid/comparable sk_argument value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0)
+			{
+				/*
+				 * Interpret result in a way that takes NEXT/PRIOR into
+				 * account
+				 */
+				if (cur->sk_flags & SK_BT_NEXT)
+					result = -1;
+				else if (cur->sk_flags & SK_BT_PRIOR)
+					result = 1;
+
+				Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP));
+			}
+		}
+		else
+		{
+			/*
+			 * Current array element/array = scan key value is a sentinel
+			 * value that represents the lowest (or highest) possible value
+			 * that's still within the range of the array.
+			 *
+			 * We don't have a valid sk_argument value from = scan key.  Check
+			 * if tupdatum is within the range of skip array instead.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(true, -dir, tupdatum, tupnull,
+									   array, cur, &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum satisfies both low_compare and high_compare, so
+				 * it's time to advance the array keys.
+				 *
+				 * Note: It's possible that the skip array will "advance" from
+				 * its MAXVAL (or MINVAL) representation to an alternative,
+				 * logically equivalent representation of the same value: a
+				 * representation where the = key gets a valid datum in its
+				 * sk_argument.  This is only possible when low_compare uses
+				 * the <= strategy (or high_compare uses the >= strategy).
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1032,18 +1581,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1068,18 +1608,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -1097,12 +1628,20 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -1178,11 +1717,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+		if (array)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->num_elems == -1)
+			{
+				/* Skip array "binary search" result determines new element */
+				_bt_skiparray_set_element(rel, cur, array, result,
+										  tupdatum, tupnull);
+			}
+			else if (array->cur_elem != set_elem)
+			{
+				/* SAOP array has new set_elem (returned by binary search) */
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
 		}
 	}
 
@@ -1575,10 +2124,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -1901,6 +2451,21 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key uses one of several sentinel values.  We just
+		 * fall back on _bt_tuple_before_array_skeys when we see such a value.
+		 */
+		if (key->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL |
+							 SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index dd6f5a15c..817ad358f 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -106,6 +106,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 2c325badf..303622f9d 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index f1e74f184..cd3ed44c8 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index e199f0716..1d3d901dc 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -371,6 +371,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index f279853de..4227ab1a7 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f23cfad71..244f48f4f 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index d3d1e485b..5c9d7c3d5 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -94,7 +94,6 @@
 
 #include "postgres.h"
 
-#include <ctype.h>
 #include <math.h>
 
 #include "access/brin.h"
@@ -193,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -214,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5759,6 +5762,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6817,6 +6906,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6826,17 +6962,21 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
+	double		correlation = 0;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
+	bool		set_correlation = false;
 	bool		eqQualHere;
-	bool		found_saop;
+	bool		found_array;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	bool		upper_inequal_col;
+	bool		lower_inequal_col;
 	double		num_sa_scans;
+	double		inequalselectivity;
 	ListCell   *lc;
 
 	/*
@@ -6852,16 +6992,21 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
-	found_saop = false;
+	found_array = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
+	upper_inequal_col = false;
+	lower_inequal_col = false;
 	num_sa_scans = 1;
+	inequalselectivity = 1;
 	foreach(lc, path->indexclauses)
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
@@ -6869,13 +7014,103 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		past_first_skipped = false;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
-			eqQualHere = false;
-			indexcol++;
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't come after a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT,
+							new_num_sa_scans;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						Assert(!set_correlation);
+						correlation = btcost_correlation(index, &vardata);
+						set_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (!past_first_skipped)
+				{
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is...
+					 */
+					if (!upper_inequal_col)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lower_inequal_col)
+						ndistinct += 1;
+
+					past_first_skipped = true;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				found_array = true;
+				new_num_sa_scans = num_sa_scans * ndistinct;
+
+				/*
+				 * Stop adding new skip arrays when the would-be new
+				 * num_sa_scans exceeds a third of the number of index pages
+				 */
+				if (ceil(index->pages * 0.3333333) < new_num_sa_scans)
+					break;
+
+				/* Done costing skipping for this index column */
+				num_sa_scans = new_num_sa_scans;
+				indexcol++;
+			}
+
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+				break;			/* no quals for indexcol (can't skip scan) */
+
+			/* reset for next indexcol */
+			eqQualHere = false;
+			upper_inequal_col = false;
+			lower_inequal_col = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6897,6 +7132,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6905,7 +7141,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				double		alength = estimate_array_length(root, other_operand);
 
 				clause_op = saop->opno;
-				found_saop = true;
+				found_array = true;
 				/* estimate SA descents by indexBoundQuals only */
 				if (alength > 1)
 					num_sa_scans *= alength;
@@ -6917,7 +7153,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skip scan purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6933,6 +7169,34 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct.
+					 *
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (!upper_inequal_col)
+							inequalselectivity =
+								Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+									DEFAULT_RANGE_INEQ_SEL);
+						upper_inequal_col = true;
+					}
+					else
+					{
+						if (!lower_inequal_col)
+							inequalselectivity =
+								Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+									DEFAULT_RANGE_INEQ_SEL);
+						lower_inequal_col = true;
+					}
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6948,7 +7212,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
-		!found_saop &&
+		!found_array &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
 	else
@@ -6988,14 +7252,18 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * of leaf pages (we make it 1/3 the total number of pages instead) to
 		 * give the btree code credit for its ability to continue on the leaf
 		 * level with low selectivity scans.
+		 *
+		 * Note: num_sa_scans is derived in a way that treats any skip scan
+		 * quals as if they were ScalarArrayOpExpr quals whose array has as
+		 * many distinct elements as possibly-matching values in the index.
 		 */
 		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
 		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
-		 * to integer.
+		 * ScalarArrayOpExpr quals (as well as any skip scan quals) included
+		 * in indexBoundQuals, and then round to integer.
 		 *
 		 * It is tempting to make genericcostestimate behave as if SAOP
 		 * clauses work in almost the same way as scalar operators during
@@ -7050,110 +7318,25 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * cost is somewhat arbitrarily set at 50x cpu_operator_cost per page
 	 * touched.  The number of such pages is btree tree height plus one (ie,
 	 * we charge for the leaf page too).  As above, charge once per estimated
-	 * SA index descent.
+	 * index descent.
 	 */
 	descentCost = (index->tree_height + 1) * DEFAULT_PAGE_CPU_MULTIPLIER * cpu_operator_cost;
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!set_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..2bd35d2d2
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns skip support struct, allocating in caller's memory
+ * context.  Otherwise returns NULL, indicating that operator class has no
+ * skip support function.
+ */
+SkipSupport
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse)
+{
+	Oid			skipSupportFunction;
+	SkipSupport sksup;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return NULL;
+
+	sksup = palloc(sizeof(SkipSupportData));
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return sksup;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index ba9bae050..f64fbf263 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2286,6 +2287,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 4f8402ef9..1b72059d2 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,6 +13,7 @@
 
 #include "postgres.h"
 
+#include <limits.h>
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
@@ -21,6 +22,7 @@
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -416,6 +418,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index d17fcbd5c..1f1aafb36 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -829,7 +829,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 053619624..7e23a7b6e 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 8879554c3..bfb1a286e 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -581,6 +581,47 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Index Only Scan using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Index Only Scan Backward using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 --
 -- Test for multilevel page deletion
 --
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index bd5f002cf..113fc3293 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2247,7 +2247,8 @@ SELECT count(*) FROM dupindexcols
 (1 row)
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 explain (costs off)
 SELECT unique1 FROM tenk1
@@ -2269,7 +2270,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2289,7 +2290,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2309,6 +2310,25 @@ ORDER BY thousand DESC, tenthous DESC;
         0 |     3000
 (2 rows)
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ Index Only Scan Backward using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > 995) AND (tenthous = ANY ('{998,999}'::integer[])))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+ thousand | tenthous 
+----------+----------
+      999 |      999
+      998 |      998
+(2 rows)
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -2339,6 +2359,45 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 ---------
 (0 rows)
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+                   QUERY PLAN                    
+-------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY (NULL::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY ('{NULL,NULL,NULL}'::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: ((unique1 IS NULL) AND (unique1 IS NULL))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+ unique1 
+---------
+(0 rows)
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
                                 QUERY PLAN                                 
@@ -2462,6 +2521,44 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 ---------
 (0 rows)
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > '-1'::integer) AND (thousand >= 0) AND (tenthous = 3000))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+(1 row)
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand < 3) AND (thousand <= 2) AND (tenthous = 1001))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        1 |     1001
+(1 row)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index f9db4032e..d86f178ad 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5325,9 +5325,10 @@ Function              | in_range(time without time zone,time without time zone,i
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql
index 670ad5c6e..68c61dbc7 100644
--- a/src/test/regress/sql/btree_index.sql
+++ b/src/test/regress/sql/btree_index.sql
@@ -327,6 +327,27 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 
 --
 -- Test for multilevel page deletion
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index be570da08..bb4782cae 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -852,7 +852,8 @@ SELECT count(*) FROM dupindexcols
   WHERE f1 BETWEEN 'WA' AND 'ZZZ' and id < 1000 and f1 ~<~ 'YX';
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 
 explain (costs off)
@@ -864,7 +865,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -874,7 +875,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -884,6 +885,15 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand DESC, tenthous DESC;
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -897,6 +907,21 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 
 SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('{33, 44}'::bigint[]);
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
 
@@ -942,6 +967,26 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
 SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9a3bee93d..a0cc20b1d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -219,6 +219,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2684,6 +2685,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.47.2

v23-0003-Lower-the-overhead-of-nbtree-runtime-skip-checks.patchapplication/octet-stream; name=v23-0003-Lower-the-overhead-of-nbtree-runtime-skip-checks.patchDownload
From 488813db6865760e0b7b7134c2b3d961af63c31a Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v23 3/5] Lower the overhead of nbtree runtime skip checks.

Add a new "skipskip" strategy to fix regressions in index scans that are
nominally eligible to use skip scan, but can never actually benefit from
skipping.  These are cases where a leading prefix column contains many
distinct values -- typically as many distinct values are there are total
index tuples.  This works by dynamically falling back on a strategy that
temporarily treats all user-supplied scan keys as if they were marked
non-required, while avoiding all skip array maintenance.  The new
optimization is applied for all pages beyond each primitive index scan's
first leaf page read.

Also add heuristics that make _bt_advance_array_keys avoid ending the
ongoing primitive index scan when it reads a leaf page beyond the first
one for the primscan.  That way we'll make better decisions about how to
start and end primitive index scans.  Note that it is important to not
start an excessive number of primitive scans for its own sake, but also
because doing poorly there risks making the trigger conditions for the
new "skipskip" optimization tend to under-apply the optimization.

Fixing (or significantly ameliorating) regressions in the worst case
like this enables skip scan's approach within the planner.  The planner
doesn't generate distinct index paths to represent nbtree index skip
scans; whether and to what extent a scan actually skips is always
determined dynamically, at runtime.  The planner considers skipping when
costing relevant index paths, but that in itself won't influence skip
scan's runtime behavior.

This approach makes skip scan adapt to skewed key distributions.  It
also makes planning less sensitive to misestimations.  An index path
with an inaccurate estimate of the total number of required primitive
index scans won't have to perform many more primitive scans at runtime
(it'll behave like a traditional full index scan instead).

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h           |  14 +-
 src/backend/access/nbtree/nbtree.c    |   8 +-
 src/backend/access/nbtree/nbtsearch.c |  69 ++--
 src/backend/access/nbtree/nbtutils.c  | 474 ++++++++++++++++++--------
 4 files changed, 391 insertions(+), 174 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 346ee92a8..e85616dc6 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1061,8 +1061,8 @@ typedef struct BTScanOpaqueData
 	int			numArrayKeys;	/* number of equality-type array keys */
 	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
-	bool		scanBehind;		/* Last array advancement matched -inf attr? */
-	bool		oppositeDirCheck;	/* explicit scanBehind recheck needed? */
+	bool		scanBehind;		/* arrays might be behind scan's key space? */
+	bool		noSkipskip;		/* don't 'skipskip' when reading next page? */
 	BTArrayKeyInfo *arrayKeys;	/* info about each equality-type array key */
 	FmgrInfo   *orderProcs;		/* ORDER procs for required equality keys */
 	MemoryContext arrayContext; /* scan-lifespan context for array data */
@@ -1115,10 +1115,13 @@ typedef struct BTReadPageState
 
 	/*
 	 * Input and output parameters, set and unset by both _bt_readpage and
-	 * _bt_checkkeys to manage precheck optimizations
+	 * _bt_checkkeys to manage precheck and skipskip optimizations
 	 */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
+	bool		skipskip;		/* skip maintenance of skip arrays? */
+	bool		firstpage;		/* on first page of current primitive scan? */
+	int			ikey;			/* start comparisons from ikey'th scan key */
 
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
@@ -1325,8 +1328,9 @@ extern int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir);
 extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 						  IndexTuple tuple, int tupnatts);
-extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
-								  IndexTuple finaltup);
+extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
+									 IndexTuple finaltup);
+extern void _bt_checkkeys_skipskip(IndexScanDesc scan, BTReadPageState *pstate);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index c44d07e7d..b936c0b71 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -349,7 +349,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 
 	so->needPrimScan = false;
 	so->scanBehind = false;
-	so->oppositeDirCheck = false;
+	so->noSkipskip = false;
 	so->arrayKeys = NULL;
 	so->orderProcs = NULL;
 	so->arrayContext = NULL;
@@ -393,7 +393,7 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 	so->markItemIndex = -1;
 	so->needPrimScan = false;
 	so->scanBehind = false;
-	so->oppositeDirCheck = false;
+	so->noSkipskip = false;
 	BTScanPosUnpinIfPinned(so->markPos);
 	BTScanPosInvalidate(so->markPos);
 
@@ -800,7 +800,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 		 */
 		so->needPrimScan = false;
 		so->scanBehind = false;
-		so->oppositeDirCheck = false;
+		so->noSkipskip = false;
 	}
 	else
 	{
@@ -860,7 +860,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 */
 			so->needPrimScan = true;
 			so->scanBehind = false;
-			so->oppositeDirCheck = false;
+			so->noSkipskip = false;
 		}
 		else if (btscan->btps_pageStatus != BTPARALLEL_ADVANCING)
 		{
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index cdbfd5a13..eb4ec693e 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1654,6 +1654,9 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.continuescan = true; /* default assumption */
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
+	pstate.skipskip = false;
+	pstate.firstpage = firstPage;
+	pstate.ikey = 0;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
@@ -1713,6 +1716,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 		pstate.continuescan = true; /* reset */
 	}
 
+	/*
+	 * Skip maintenance of skip arrays (if any) during primitive index scans
+	 * that read leaf pages after the first
+	 */
+	else if (!firstPage && so->skipScan && !so->noSkipskip && minoff < maxoff)
+		_bt_checkkeys_skipskip(scan, &pstate);
+
 	if (ScanDirectionIsForward(dir))
 	{
 		/* SK_SEARCHARRAY forward scans must provide high key up front */
@@ -1722,29 +1732,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 
 			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
 
-			if (unlikely(so->oppositeDirCheck))
+			if (unlikely(so->scanBehind) &&
+				!_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup))
 			{
-				Assert(so->scanBehind);
-
-				/*
-				 * Last _bt_readpage call scheduled a recheck of finaltup for
-				 * required scan keys up to and including a > or >= scan key.
-				 *
-				 * _bt_checkkeys won't consider the scanBehind flag unless the
-				 * scan is stopped by a scan key required in the current scan
-				 * direction.  We need this recheck so that we'll notice when
-				 * all tuples on this page are still before the _bt_first-wise
-				 * start of matches for the current set of array keys.
-				 */
-				if (!_bt_oppodir_checkkeys(scan, dir, pstate.finaltup))
-				{
-					/* Schedule another primitive index scan after all */
-					so->currPos.moreRight = false;
-					so->needPrimScan = true;
-					return false;
-				}
-
-				/* Deliberately don't unset scanBehind flag just yet */
+				/* Schedule another primitive index scan after all */
+				so->currPos.moreRight = false;
+				so->needPrimScan = true;
+				return false;
 			}
 		}
 
@@ -1784,6 +1778,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum < pstate.skip);
+				Assert(!pstate.skipskip);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1849,6 +1844,16 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
+			if (pstate.skipskip)
+			{
+				/*
+				 * reset array keys for finaltup call, since skipskip
+				 * optimization prevented ordinary array maintenance
+				 */
+				Assert(so->skipScan);
+				pstate.skipskip = false;
+				pstate.ikey = 0;
+			}
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -1868,6 +1873,15 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			ItemId		iid = PageGetItemId(page, minoff);
 
 			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+			if (unlikely(so->scanBehind) &&
+				!_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup))
+			{
+				/* Schedule another primitive index scan after all */
+				so->currPos.moreLeft = false;
+				so->needPrimScan = true;
+				return false;
+			}
 		}
 
 		/* load items[] in descending order */
@@ -1909,6 +1923,16 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff && pstate.skipskip)
+			{
+				/*
+				 * reset array keys for finaltup call, since skipskip
+				 * optimization prevented ordinary array maintenance
+				 */
+				Assert(so->skipScan);
+				pstate.skipskip = false;
+				pstate.ikey = 0;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
@@ -1920,6 +1944,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum > pstate.skip);
+				Assert(!pstate.skipskip);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index c46745e9b..24d87620b 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -55,11 +55,12 @@ static bool _bt_verify_keys_with_arraykeys(IndexScanDesc scan);
 #endif
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool skipskip,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-								 ScanDirection dir, bool *continuescan);
+								 ScanDirection dir, bool skipskip, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -600,7 +601,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 		_bt_array_set_low_or_high(rel, skey, array,
 								  ScanDirectionIsForward(dir));
 	}
-	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
+	so->scanBehind = so->noSkipskip = false;	/* reset */
 }
 
 /*
@@ -1295,7 +1296,7 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
 
 	Assert(so->numArrayKeys);
 
-	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
+	so->scanBehind = so->noSkipskip = false;	/* reset */
 
 	/*
 	 * Array keys are advanced within _bt_checkkeys when the scan reaches the
@@ -1381,14 +1382,14 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
  * postcondition's <= operator with a >=.  In other words, just swap the
  * precondition with the postcondition.)
  *
- * We also deal with "advancing" non-required arrays here.  Callers whose
- * sktrig scan key is non-required specify sktrig_required=false.  These calls
- * are the only exception to the general rule about always advancing the
- * required array keys (the scan may not even have a required array).  These
- * callers should just pass a NULL pstate (since there is never any question
- * of stopping the scan).  No call to _bt_tuple_before_array_skeys is required
- * ahead of these calls (it's already clear that any required scan keys must
- * be satisfied by caller's tuple).
+ * We also deal with "advancing" non-required arrays here (or arrays that are
+ * temporarily being treated as non-required due to the application of the
+ * "skipskip" optimization).  Callers whose sktrig scan key is non-required
+ * specify sktrig_required=false.  These calls are the only exception to the
+ * general rule about always advancing the required array keys (the scan may
+ * not even have a required array).  These callers should just pass a NULL
+ * pstate (since there is never any question of stopping the scan).  No call
+ * to _bt_tuple_before_array_skeys is required ahead of these calls.
  *
  * Note that we deal with non-array required equality strategy scan keys as
  * degenerate single element arrays here.  Obviously, they can never really
@@ -1424,10 +1425,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 				all_satisfied = true;
 
 	/*
-	 * Unset so->scanBehind (and so->oppositeDirCheck) in case they're still
+	 * Unset so->scanBehind (as well as so->noSkipskip) in case they're still
 	 * set from back when we dealt with the previous page's high key/finaltup
 	 */
-	so->scanBehind = so->oppositeDirCheck = false;
+	so->scanBehind = so->noSkipskip = false;
 
 	if (sktrig_required)
 	{
@@ -1445,8 +1446,8 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		pstate->firstmatch = false;
 
-		/* Shouldn't have to invalidate 'prechecked', though */
-		Assert(!pstate->prechecked);
+		/* Shouldn't have to invalidate 'prechecked' or 'skipskip' or 'ikey' */
+		Assert(!pstate->prechecked && !pstate->skipskip && pstate->ikey == 0);
 
 		/*
 		 * Once we return we'll have a new set of required array keys, so
@@ -1498,8 +1499,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -1645,7 +1644,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -1687,7 +1686,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -1702,7 +1701,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -1762,6 +1761,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * of any required scan key).  All that matters is whether caller's tuple
 	 * satisfies the new qual, so it's safe to just skip the _bt_check_compare
 	 * recheck when we've already determined that it can only return 'false'.
+	 *
+	 * Note: in practice non-required arrays are usually just arrays that
+	 * we're treating as required for callers using the skipskip optimization.
+	 * Their scan keys are marked required, but they're non-required to us.
 	 */
 	if ((sktrig_required && all_required_satisfied) ||
 		(!sktrig_required && all_satisfied))
@@ -1773,7 +1776,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -1882,14 +1885,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * Note: if so->scanBehind hasn't already been set for finaltup by us,
 	 * it'll be set during this call to _bt_tuple_before_array_skeys.  Either
 	 * way, it'll be set correctly (for the whole page) after this point.
-	 */
-	if (!all_required_satisfied && pstate->finaltup &&
-		_bt_tuple_before_array_skeys(scan, dir, pstate->finaltup, tupdesc,
-									 BTreeTupleGetNAtts(pstate->finaltup, rel),
-									 false, 0, &so->scanBehind))
-		goto new_prim_scan;
-
-	/*
+	 *
 	 * When we encounter a truncated finaltup high key attribute, we're
 	 * optimistic about the chances of its corresponding required scan key
 	 * being satisfied when we go on to check it against tuples from this
@@ -1900,10 +1896,11 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * keys for one or more truncated attribute values (scan keys required in
 	 * _either_ scan direction).
 	 *
-	 * There is a chance that _bt_checkkeys (which checks so->scanBehind) will
-	 * find that even the sibling leaf page's finaltup is < the new array
-	 * keys.  When that happens, our optimistic policy will have incurred a
-	 * single extra leaf page access that could have been avoided.
+	 * There is a chance that our _bt_scanbehind_checkkeys call (made once on
+	 * the next page when so->scanBehind is set here by us) will find that
+	 * even the sibling leaf page's finaltup is < the new array keys.  When
+	 * that happens, our optimistic policy will have incurred a single extra
+	 * leaf page access that could have been avoided.
 	 *
 	 * A pessimistic policy would give backward scans a gratuitous advantage
 	 * over forward scans.  We'd punish forward scans for applying more
@@ -1917,26 +1914,11 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * untruncated prefix of attributes must strictly satisfy the new qual
 	 * (though it's okay if any non-required scan keys fail to be satisfied).
 	 */
-	if (so->scanBehind && has_required_opposite_direction_only)
-	{
-		/*
-		 * However, we need to work harder whenever the scan involves a scan
-		 * key required in the opposite direction to the scan only, along with
-		 * a finaltup with at least one truncated attribute that's associated
-		 * with a scan key marked required (required in either direction).
-		 *
-		 * _bt_check_compare simply won't stop the scan for a scan key that's
-		 * marked required in the opposite scan direction only.  That leaves
-		 * us without an automatic way of reconsidering any opposite-direction
-		 * inequalities if it turns out that starting a new primitive index
-		 * scan will allow _bt_first to skip ahead by a great many leaf pages.
-		 *
-		 * We deal with this by explicitly scheduling a finaltup recheck on
-		 * the right sibling page.  _bt_readpage calls _bt_oppodir_checkkeys
-		 * for next page's finaltup (and we skip it for this page's finaltup).
-		 */
-		so->oppositeDirCheck = true;	/* recheck next page's high key */
-	}
+	if (!all_required_satisfied && pstate->finaltup &&
+		_bt_tuple_before_array_skeys(scan, dir, pstate->finaltup, tupdesc,
+									 BTreeTupleGetNAtts(pstate->finaltup, rel),
+									 false, 0, &so->scanBehind))
+		goto new_prim_scan;
 
 	/*
 	 * Handle inequalities marked required in the opposite scan direction.
@@ -1974,9 +1956,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * at least suggests many more skippable pages beyond the current page.
 	 * (when so->oppositeDirCheck was set, this'll happen on the next page.)
 	 */
-	else if (has_required_opposite_direction_only && pstate->finaltup &&
-			 (all_required_satisfied || oppodir_inequality_sktrig) &&
-			 unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup)))
+	if (has_required_opposite_direction_only &&
+		(all_required_satisfied || oppodir_inequality_sktrig) &&
+		pstate->finaltup && !so->scanBehind &&
+		unlikely(!_bt_scanbehind_checkkeys(scan, dir, pstate->finaltup)))
 	{
 		/*
 		 * Make sure that any non-required arrays are set to the first array
@@ -1986,6 +1969,8 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		goto new_prim_scan;
 	}
 
+continue_scan:
+
 	/*
 	 * Stick with the ongoing primitive index scan for now.
 	 *
@@ -2007,8 +1992,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	if (so->scanBehind)
 	{
 		/* Optimization: skip by setting "look ahead" mechanism's offnum */
-		Assert(ScanDirectionIsForward(dir));
-		pstate->skip = pstate->maxoff + 1;
+		if (ScanDirectionIsForward(dir))
+			pstate->skip = pstate->maxoff + 1;
+		else
+			pstate->skip = pstate->minoff - 1;
 	}
 
 	/* Caller's tuple doesn't match the new qual */
@@ -2018,6 +2005,47 @@ new_prim_scan:
 
 	Assert(pstate->finaltup);	/* not on rightmost/leftmost page */
 
+	/*
+	 * Looks like another primitive index scan is required.  But consider
+	 * backing out and continuing the ongoing primscan based on scan-level
+	 * heuristics.  This is particularly important with scans that have skip
+	 * arrays, where subsets of the index may have tuples with many distinct
+	 * values in respect of an attribute constrained by a skip array.
+	 *
+	 * Continue the ongoing primitive scan (but schedule a recheck for when
+	 * the scan arrives on the next sibling leaf page) when the scan has
+	 * already read at least one leaf page before the one we're reading now.
+	 */
+	if (!pstate->firstpage)
+	{
+		/* Schedule a recheck once on the next (or previous) page */
+		so->scanBehind = true;
+
+		/*
+		 * The skipskip optimization (used only by scans with skip arrays) is
+		 * also only applied when reading a page that isn't the first page
+		 * read by the ongoing primitive index scan.  It doesn't usually make
+		 * sense to apply the skipskip optimization just because we forced the
+		 * scan to continue to the next page.  If we end up here then we're
+		 * likely to be scanning pages where some amount of useful skipping is
+		 * possible via the "look ahead" optimization (both optimizations can
+		 * never be applied at the same time, and we usually expect to benefit
+		 * from making sure that the lookahead optimization can be used when
+		 * the scan has reached the next leaf page).
+		 *
+		 * Sometimes it makes sense to allow the skipskip optimization to be
+		 * used in spite of our forcing the scan to continue here, though.
+		 * Only suppress the skipskip optimization on the next page when we
+		 * reached here before reaching the page's finaltup.  This heuristic
+		 * tends to help most during backwards scans, where finaltup won't
+		 * give us a useful preview of what's on the previous/sibling page.
+		 */
+		so->noSkipskip = tuple != pstate->finaltup;
+
+		/* Continue the current primitive scan */
+		goto continue_scan;
+	}
+
 	/*
 	 * End this primitive index scan, but schedule another.
 	 *
@@ -2180,13 +2208,14 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	TupleDesc	tupdesc = RelationGetDescr(scan->indexRelation);
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ScanDirection dir = so->currPos.dir;
-	int			ikey = 0;
+	int			ikey = pstate->ikey;
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+							arrayKeys, pstate->skipskip,
+							pstate->prechecked, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
@@ -2197,22 +2226,23 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 *
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
-		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
+		Assert(!so->scanBehind && !so->noSkipskip);
+		Assert(!pstate->skipskip && !pstate->prechecked &&
+			   !pstate->firstmatch);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
 	if (pstate->prechecked || pstate->firstmatch)
 	{
 		bool		dcontinuescan;
-		int			dikey = 0;
+		int			dikey = pstate->ikey;
 
 		/*
 		 * Call relied on continuescan/firstmatch prechecks -- assert that we
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, pstate->skipskip, false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -2235,65 +2265,40 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * It's also possible that the scan is still _before_ the _start_ of
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
+	Assert(!pstate->skipskip);
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
+		/* Override _bt_check_compare, continue primitive scan */
+		pstate->continuescan = true;
+
 		/*
-		 * Tuple is still before the start of matches according to the scan's
-		 * required array keys (according to _all_ of its required equality
-		 * strategy keys, actually).
+		 * We will end up here repeatedly given a group of tuples > the
+		 * previous array keys and < the now-current keys (for a backwards
+		 * scan it's just the same, though the operators swap positions).
 		 *
-		 * _bt_advance_array_keys occasionally sets so->scanBehind to signal
-		 * that the scan's current position/tuples might be significantly
-		 * behind (multiple pages behind) its current array keys.  When this
-		 * happens, we need to be prepared to recover by starting a new
-		 * primitive index scan here, on our own.
+		 * We must avoid allowing this linear search process to scan very many
+		 * tuples from well before the start of tuples matching the current
+		 * array keys (or from well before the point where we'll once again
+		 * have to advance the scan's array keys).
+		 *
+		 * We keep the overhead under control by speculatively "looking ahead"
+		 * to later still-unscanned items from this same leaf page. We'll only
+		 * attempt this once the number of tuples that the linear search
+		 * process has examined starts to get out of hand.
 		 */
-		Assert(!so->scanBehind ||
-			   so->keyData[ikey].sk_strategy == BTEqualStrategyNumber);
-		if (unlikely(so->scanBehind) && pstate->finaltup &&
-			_bt_tuple_before_array_skeys(scan, dir, pstate->finaltup, tupdesc,
-										 BTreeTupleGetNAtts(pstate->finaltup,
-															scan->indexRelation),
-										 false, 0, NULL))
+		pstate->rechecks++;
+		if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
 		{
-			/* Cut our losses -- start a new primitive index scan now */
-			pstate->continuescan = false;
-			so->needPrimScan = true;
-		}
-		else
-		{
-			/* Override _bt_check_compare, continue primitive scan */
-			pstate->continuescan = true;
+			/* See if we should skip ahead within the current leaf page */
+			_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
 
 			/*
-			 * We will end up here repeatedly given a group of tuples > the
-			 * previous array keys and < the now-current keys (for a backwards
-			 * scan it's just the same, though the operators swap positions).
-			 *
-			 * We must avoid allowing this linear search process to scan very
-			 * many tuples from well before the start of tuples matching the
-			 * current array keys (or from well before the point where we'll
-			 * once again have to advance the scan's array keys).
-			 *
-			 * We keep the overhead under control by speculatively "looking
-			 * ahead" to later still-unscanned items from this same leaf page.
-			 * We'll only attempt this once the number of tuples that the
-			 * linear search process has examined starts to get out of hand.
+			 * Might have set pstate.skip to a later page offset.  When that
+			 * happens then _bt_readpage caller will inexpensively skip ahead
+			 * to a later tuple from the same page (the one just after the
+			 * tuple we successfully "looked ahead" to).
 			 */
-			pstate->rechecks++;
-			if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
-			{
-				/* See if we should skip ahead within the current leaf page */
-				_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
-
-				/*
-				 * Might have set pstate.skip to a later page offset.  When
-				 * that happens then _bt_readpage caller will inexpensively
-				 * skip ahead to a later tuple from the same page (the one
-				 * just after the tuple we successfully "looked ahead" to).
-				 */
-			}
 		}
 
 		/* This indextuple doesn't match the current qual, in any case */
@@ -2311,26 +2316,20 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 }
 
 /*
- * Test whether an indextuple fails to satisfy an inequality required in the
- * opposite direction only.
+ * Test whether finaltup (the final tuple on the page) is still before the
+ * start of matches for the current array keys.
  *
  * Caller's finaltup tuple is the page high key (for forwards scans), or the
  * first non-pivot tuple (for backwards scans).  Called during scans with
- * required array keys and required opposite-direction inequalities.
+ * array keys when the so->scanBehind flag was set on the previous page.
  *
- * Returns false if an inequality scan key required in the opposite direction
- * only isn't satisfied (and any earlier required scan keys are satisfied).
+ * Returns false if the tuple is still before the start of matches.  When that
+ * happens caller should cut its losses and start a new primitive index scan.
  * Otherwise returns true.
- *
- * An unsatisfied inequality required in the opposite direction only might
- * well enable skipping over many leaf pages, provided another _bt_first call
- * takes place.  This type of unsatisfied inequality won't usually cause
- * _bt_checkkeys to stop the scan to consider array advancement/starting a new
- * primitive index scan.
  */
 bool
-_bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
-					  IndexTuple finaltup)
+_bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
+						 IndexTuple finaltup)
 {
 	Relation	rel = scan->indexRelation;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2342,8 +2341,21 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 
 	Assert(so->numArrayKeys);
 
+	if (_bt_tuple_before_array_skeys(scan, dir, finaltup, tupdesc,
+									 nfinaltupatts, false, 0, NULL))
+		return false;
+
+	/*
+	 * An unsatisfied inequality required in the opposite direction only might
+	 * well enable skipping over many leaf pages, provided another _bt_first
+	 * call takes place.  This type of unsatisfied inequality won't usually
+	 * cause _bt_checkkeys to stop the scan to consider array
+	 * advancement/starting a new primitive index scan.
+	 *
+	 * XXX We should avoid doing this unless it's truly necessary.
+	 */
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -2382,17 +2394,24 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Though we advance non-required array keys on our own, that shouldn't have
  * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
+ * have no fixed relationship with the scan's progress.  (skipskip=true makes
+ * us treat required scan keys as non-required, though.  That's safe because
+ * _bt_readpage will account for our failure to fully maintain the scan's
+ * arrays later on, right before its finaltup call to _bt_checkkeys.)
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass skipskip=true to instruct us to skip all of the scan's skip arrays.
+ * This provides the scan with a way of keeping the cost of maintaining its
+ * skip arrays under control, given skip arrays on high cardinality columns
+ * (i.e. given a skip array on a column which isn't a good fit for skip scan).
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool skipskip,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -2409,10 +2428,18 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when reading a page that's now using the "skipskip" optimization)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (skipskip)
+		{
+			Assert(!prechecked);
+
+			if (key->sk_flags & SK_BT_SKIP)
+				continue;
+		}
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
@@ -2470,7 +2497,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
-									 continuescan))
+									 skipskip, continuescan))
 				continue;
 			return false;
 		}
@@ -2525,7 +2552,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -2543,7 +2570,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -2612,7 +2639,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
  */
 static bool
 _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
-					 TupleDesc tupdesc, ScanDirection dir, bool *continuescan)
+					 TupleDesc tupdesc, ScanDirection dir, bool skipskip,
+					 bool *continuescan)
 {
 	ScanKey		subkey = (ScanKey) DatumGetPointer(skey->sk_argument);
 	int32		cmpresult = 0;
@@ -2652,7 +2680,11 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 
 		if (isNull)
 		{
-			if (subkey->sk_flags & SK_BT_NULLS_FIRST)
+			if (skipskip)
+			{
+				/* treat scan key as non-required during skipskip calls */
+			}
+			else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -2706,8 +2738,12 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			 */
 			Assert(subkey != (ScanKey) DatumGetPointer(skey->sk_argument));
 			subkey--;
-			if ((subkey->sk_flags & SK_BT_REQFWD) &&
-				ScanDirectionIsForward(dir))
+			if (skipskip)
+			{
+				/* treat scan key as non-required during skipskip calls */
+			}
+			else if ((subkey->sk_flags & SK_BT_REQFWD) &&
+					 ScanDirectionIsForward(dir))
 				*continuescan = false;
 			else if ((subkey->sk_flags & SK_BT_REQBKWD) &&
 					 ScanDirectionIsBackward(dir))
@@ -2759,7 +2795,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			break;
 	}
 
-	if (!result)
+	if (!result && !skipskip)
 	{
 		/*
 		 * Tuple fails this qual.  If it's a required qual for the current
@@ -2803,6 +2839,8 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	Assert(!pstate->skipskip);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
@@ -2863,6 +2901,156 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	}
 }
 
+/*
+ * Determine if a scan with skip array keys should avoid evaluating its skip
+ * arrays, plus any scan keys covering a prefix of unchanging attribute values
+ * on caller's page.
+ *
+ * Called at the start of a _bt_readpage call for the second or subsequent
+ * leaf page scanned by a primitive index scan (that uses skip scan).
+ *
+ * Sets pstate.skipskip when it's safe for _bt_readpage caller to apply the
+ * 'skipskip' optimization on this page during skip scans.
+ */
+void
+_bt_checkkeys_skipskip(IndexScanDesc scan, BTReadPageState *pstate)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	ScanDirection dir = so->currPos.dir;
+	ItemId		iid;
+	IndexTuple	firsttup,
+				lasttup;
+	int			arrayidx = 0,
+				set_ikey = 0,
+				firstunequalattnum;
+	bool		nonsharedprefix_nonskip = false,
+				nonsharedprefix_range_skip = false,
+				ikeyset = false;
+
+	/* Only called during skip scans */
+	Assert(so->skipScan && !so->noSkipskip && !pstate->skipskip);
+
+	/* Can't combine 'skipskip' with the similar 'precheck' optimization */
+	Assert(!pstate->prechecked);
+
+	Assert(pstate->minoff < pstate->maxoff);
+	if (ScanDirectionIsForward(dir))
+	{
+		iid = PageGetItemId(pstate->page, pstate->minoff);
+		firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+		iid = PageGetItemId(pstate->page, pstate->maxoff);
+		lasttup = (IndexTuple) PageGetItem(pstate->page, iid);
+	}
+	else
+	{
+		iid = PageGetItemId(pstate->page, pstate->maxoff);
+		firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+		iid = PageGetItemId(pstate->page, pstate->minoff);
+		lasttup = (IndexTuple) PageGetItem(pstate->page, iid);
+	}
+
+	Assert(!BTreeTupleIsPivot(firsttup) && !BTreeTupleIsPivot(lasttup));
+	Assert(firsttup != lasttup);
+
+	firstunequalattnum = 1;
+	if (so->numberOfKeys > 2)
+		firstunequalattnum = _bt_keep_natts_fast(rel, firsttup, lasttup);
+
+	for (int ikey = 0; ikey < so->numberOfKeys; ikey++)
+	{
+		ScanKey		cur = so->keyData + ikey;
+		BTArrayKeyInfo *array = NULL;
+		Datum		tupdatum;
+		bool		tupnull;
+		int32		result;
+
+		if (cur->sk_attno >= firstunequalattnum)
+		{
+			if (!ikeyset || !nonsharedprefix_nonskip)
+			{
+				ikeyset = true;
+				set_ikey = ikey;
+			}
+			if (!(cur->sk_flags & SK_BT_SKIP))
+				nonsharedprefix_nonskip = true;
+		}
+
+		if (cur->sk_strategy != BTEqualStrategyNumber)
+			break;
+		if (!(cur->sk_flags & SK_SEARCHARRAY))
+		{
+			if (cur->sk_attno < firstunequalattnum)
+			{
+				tupdatum = index_getattr(firsttup, cur->sk_attno, tupdesc, &tupnull);
+
+				/* Scankey has a valid/comparable sk_argument value */
+				result = _bt_compare_array_skey(&so->orderProcs[ikey],
+												tupdatum, tupnull,
+												cur->sk_argument, cur);
+				if (result != 0)
+				{
+					ikeyset = true;
+					nonsharedprefix_nonskip = true;
+				}
+
+			}
+			continue;
+		}
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == ikey);
+
+		/*
+		 * Only need to maintain set_ikey and current so->arrayKeys[] offset,
+		 * unless dealing with a range skip array
+		 */
+		if (array->num_elems != -1 || array->null_elem)
+			continue;
+
+		/*
+		 * Found a range skip array to test.
+		 *
+		 * If this is the first range skip array, it is still safe to apply
+		 * the skipskip optimization.  If this is the second or subsequent
+		 * range skip array, then it is only safe if there is no more than one
+		 * range skip array on an attribute whose values change on this page.
+		 *
+		 * Note: we deliberately ignore regular (non-range) skip arrays, since
+		 * they're always satisfied by any possible attribute value.
+		 */
+		if (nonsharedprefix_range_skip)
+			return;
+		if (cur->sk_attno < firstunequalattnum)
+		{
+			/*
+			 * This range skip array doesn't count towards our "no more than
+			 * one range skip array" limit -- but it must still be satisfied
+			 * by both firsttup and finaltup
+			 */
+		}
+		else
+			nonsharedprefix_range_skip = true;
+
+		/* Test the first/lower bound non-pivot tuple on the page */
+		tupdatum = index_getattr(firsttup, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(false, dir, tupdatum, tupnull, array, cur,
+								   &result);
+		if (result != 0)
+			return;
+
+		/* Test the page's finaltup (unless attribute is truncated) */
+		tupdatum = index_getattr(lasttup, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(false, dir, tupdatum, tupnull, array, cur,
+								   &result);
+		if (result != 0)
+			return;
+	}
+
+	pstate->ikey = set_ikey;
+	pstate->skipskip = true;
+}
+
 /*
  * _bt_killitems - set LP_DEAD state for items an indexscan caller has
  * told us were killed
-- 
2.47.2

v23-0005-DEBUG-Add-skip-scan-disable-GUCs.patchapplication/octet-stream; name=v23-0005-DEBUG-Add-skip-scan-disable-GUCs.patchDownload
From f7ddc434a475790d3aed5489da6ce68c63c98cdc Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 18 Jan 2025 10:54:44 -0500
Subject: [PATCH v23 5/5] DEBUG: Add skip scan disable GUCs.

---
 src/include/access/nbtree.h                   |  4 +++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 31 +++++++++++++++++++
 src/backend/utils/misc/guc_tables.c           | 23 ++++++++++++++
 3 files changed, 58 insertions(+)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index e85616dc6..d907a7a2e 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1184,6 +1184,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index e284f0c11..67c8f1aa5 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -21,6 +21,27 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 typedef struct BTScanKeyPreproc
 {
 	ScanKey		inkey;
@@ -1634,6 +1655,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
 			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
 
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].sksup = NULL;
+
 			/*
 			 * We'll need a 3-way ORDER proc to perform "binary searches" for
 			 * the next matching array element.  Set that up now.
@@ -2139,6 +2164,12 @@ _bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
 		if (attno_has_rowcompare)
 			break;
 
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
 		/*
 		 * Now consider next attno_inkey (or keep going if this is an
 		 * additional scan key against the same attribute)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index b887d3e59..3fa6d4ad5 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1762,6 +1763,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3628,6 +3640,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
-- 
2.47.2

v23-0004-Apply-low-order-skip-key-in-_bt_first-more-often.patchapplication/octet-stream; name=v23-0004-Apply-low-order-skip-key-in-_bt_first-more-often.patchDownload
From 6493e2871a92631c1f7beef70890530c720f038f Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 13 Jan 2025 16:08:32 -0500
Subject: [PATCH v23 4/5] Apply low-order skip key in _bt_first more often.

Convert low_compare and high_compare nbtree skip array inequalities
(with opclasses that offer skip support) such that _bt_first will always
be able to use any low-order keys in _bt_first.

For example, an index qual "WHERE a > 5 AND b = 2" is now converted to
"WHERE a >= 6 AND b = 2" by a new preprocessing step that takes place
after a final low_compare and/or high_compare are chosen by all earlier
preprocessing steps.  That way the scan's initial call to _bt_first will
use "WHERE a >= 6 AND b = 2" to find the initial leaf level position,
rather than merely using "WHERE a > 5" ("b = 2" can always be included).
In practice this transformation has a decent chance of enabling nbtree
to avoid an extra _bt_first call just to determine the lowest-sorting
"a" value in the index (the lowest that still satisfies "WHERE a > 5").

This transformation process can only lower the total number of index
pages read when the use of a more restrictive set of initial positioning
keys in _bt_first actually allows the scan to land on some later leaf
page directly, relative to the unoptimized case (or on an earlier leaf
page directly, when we're scanning backwards).  The savings can be far
greater when affected skip arrays come after some higher-order array.
For example, a qual "WHERE x IN (1, 2, 3) AND y > 5 AND z = 2" can now
save as many as 3 _bt_first calls as a result of these transformations
(there can be as many as 1 _bt_first call saved per "x" array element).

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=FJ78K3WsF3iWNxWnUCY9f=Jdg3QPxaXE=uYUbmuRz5Q@mail.gmail.com
---
 src/backend/access/nbtree/nbtpreprocesskeys.c | 175 ++++++++++++++++++
 src/test/regress/expected/create_index.out    |  21 +++
 src/test/regress/sql/create_index.sql         |  10 +
 3 files changed, 206 insertions(+)

diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 2dd4b592c..e284f0c11 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -50,6 +50,12 @@ static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
 								 BTArrayKeyInfo *array, bool *qual_ok);
 static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
 								 BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+									   BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
@@ -1286,6 +1292,166 @@ _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
 	return true;
 }
 
+/*
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts their
+ * operator strategies, while changing the operator as appropriate).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value MINVAL, using a transformed >= key
+ * instead of using the original > key makes it safe to include lower-order
+ * scan keys in the insertion scan key (there must be lower-order scan keys
+ * after the skip array).  We will avoid an extra _bt_first to find the first
+ * value in the index > sk_argument -- at least when the first real matching
+ * value in the index happens to be an exact match for the sk_argument value
+ * that we produced here by incrementing the original input key's sk_argument.
+ * (Backwards scans derive the same benefit when they encounter the sentinel
+ * value MAXVAL, by converting the high_compare key from < to <=.)
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01'.
+ */
+static void
+_bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+						   BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	/*
+	 * Called last among all preprocessing steps, when the skip array's final
+	 * low_compare and high_compare have both been chosen
+	 */
+	Assert(so->skipScan);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1 && !array->null_elem && array->sksup);
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skiparray_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skiparray_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1822,6 +1988,15 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && array->sksup &&
+						!array->null_elem)
+						_bt_skiparray_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 113fc3293..4488fa45b 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2559,6 +2559,27 @@ ORDER BY thousand;
         1 |     1001
 (1 row)
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+                                            QUERY PLAN                                            
+--------------------------------------------------------------------------------------------------
+ Limit
+   ->  Index Only Scan using tenk1_thous_tenthous on tenk1
+         Index Cond: ((thousand > '-1'::integer) AND (tenthous = ANY ('{1001,3000}'::integer[])))
+(3 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+        1 |     1001
+(2 rows)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index bb4782cae..ef9639be9 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -987,6 +987,16 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
 ORDER BY thousand;
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
-- 
2.47.2

v23-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v23-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From c4cd3d6ebbda97e48504e047f75d01dd81fd0438 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v23 1/5] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 contrib/bloom/blscan.c                        |   1 +
 doc/src/sgml/bloom.sgml                       |   7 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 22 files changed, 244 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index dc6e01842..0d1ad8379 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -147,6 +147,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index ccf824bbd..f6014d85d 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -587,6 +587,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index 7d1e66152..81440a283 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index cc40e928e..609e85fda 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index a3a1fccf3..c4f730437 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 07bae342e..369e39bc4 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 971405e89..efcbf8828 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -70,6 +70,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -555,6 +556,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -581,6 +583,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -708,6 +711,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			Assert(btscan->btps_nextScanPage != P_NONE);
 			*next_scan_page = btscan->btps_nextScanPage;
@@ -808,6 +816,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -842,6 +852,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 472ce06f1..941b4eaaf 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -950,6 +950,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 53f910e9d..8554b4535 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c24e66f82..6b65037cd 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2105,6 +2107,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2118,6 +2121,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2134,6 +2138,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2652,6 +2657,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index bf801fe78..472169d61 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -116,6 +116,7 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++)
 	{
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 6a8a60b8c..4cddfaae1 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -174,9 +174,10 @@ CREATE INDEX
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..178436.00 rows=1 width=0) (actual time=20.005..20.005 rows=2300 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
          Buffers: shared hit=19608
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 22.632 ms
-(10 rows)
+(11 rows)
 </programlisting>
   </para>
 
@@ -209,12 +210,14 @@ CREATE INDEX
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..4.52 rows=11 width=0) (actual time=0.026..0.026 rows=7 loops=1)
                Index Cond: (i5 = 123451)
                Buffers: shared hit=3
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..4.52 rows=11 width=0) (actual time=0.007..0.007 rows=8 loops=1)
                Index Cond: (i2 = 898732)
                Buffers: shared hit=3
+               Index Searches: 1
  Planning Time: 0.264 ms
  Execution Time: 0.047 ms
-(13 rows)
+(15 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index edc2470bc..4aa7dcd65 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4262,12 +4262,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aab..34225c010 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -730,9 +730,11 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
                Buffers: shared hit=2
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
          Buffers: shared hit=24 read=6
+         Index Searches: 1
  Planning:
    Buffers: shared hit=15 dirtied=9
  Planning Time: 0.485 ms
@@ -791,6 +793,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
                            Buffers: shared hit=2
+                           Index Searches: 1
  Planning:
    Buffers: shared hit=12
  Planning Time: 0.187 ms
@@ -860,6 +863,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
    Buffers: shared hit=1
  Planning Time: 0.039 ms
@@ -894,8 +898,10 @@ EXPLAIN (ANALYZE, BUFFERS OFF) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND un
    -&gt;  BitmapAnd  (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
  Planning Time: 0.162 ms
  Execution Time: 0.143 ms
 </screen>
@@ -924,6 +930,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
                Buffers: shared read=2
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1059,6 +1066,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
    Buffers: shared hit=16
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
          Buffers: shared hit=16
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index 6361a14e6..6fc9bfedc 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -506,9 +506,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
          Buffers: shared hit=4
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(9 rows)
+(10 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 9fdf8b1d9..80a63b5f1 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index f2d146581..a5df4107a 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 5ecf971da..d9df5c0e1 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index f0707e7f7..6136a7024 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2369,6 +2369,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2686,47 +2690,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2742,16 +2755,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2770,7 +2786,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2786,16 +2802,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2816,7 +2835,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2887,16 +2906,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2904,17 +2926,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2990,17 +3015,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -3011,17 +3042,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3056,17 +3093,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3077,17 +3120,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3141,17 +3190,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3173,17 +3228,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3511,12 +3572,14 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3532,9 +3595,10 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3582,13 +3646,17 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4158,14 +4226,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 88911ca2b..cde2eac73 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index d5aab4e56..2197b0ac5 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index ea9a4fe4a..7cddf2795 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -588,6 +588,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.47.2

In reply to: Peter Geoghegan (#63)
6 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Feb 5, 2025 at 6:43 PM Peter Geoghegan <pg@bowt.ie> wrote:

The way that the "skipskip" optimization works is unchanged in v23.
And even the way that we decide whether to apply that optimization
didn't really change, either. What's new in v23 is that
v23-0003-*patch adds rules around primitive scan scheduling.
Obviously, I specifically targeted Heikki's regression when I came up
with this, but the new _bt_advance_array_keys rules are nevertheless
orthogonal: even scans that just use conventional SAOP arrays will
also use these new _bt_advance_array_keys heuristics (though it'll
tend to matter much less there).

Attached is v24, which breaks out these recent changes to primscan
scheduling into their own commit/patch (namely
v24-0002-Improve-nbtree-SAOP-primitive-scan-scheduling.patch). The
primscan scheduling improvement stuff hasn't really changed since
v23, though (though I did polish it some more). I hope to be able to
commit this new primscan scheduling patch sooner rather than later
(though I don't think that it's quite committable yet). It is
technically an independent improvement to the scheduling of primitive
index scans during SAOP nbtree index scans, so it makes sense to get
it out of the way.

The changes in v24 aren't just structural. The real changes in v24 are
to the optimization previously known as the "skipskip" optimization,
which now appears alone in
v24-0004-Lower-the-overhead-of-nbtree-runtime-skip-checks.patch (since
I broke out the other changes into their own patch). I guess it's
called the "_bt_skip_ikeyprefix" optimization now, since that's the
name of the function that now activates the optimization (the function
that is sometimes called from _bt_readpage during so->skipScan scans).
It seemed better to place the emphasis on starting calls to
_bt_check_compare with a later pstate.ikey (i.e. one greater than 0),
since that's where most of the benefit comes from.

We now treat all of the scan's arrays as nonrequired when the
_bt_skip_ikeyprefix optimization is activated -- even skip arrays can
have nonrequired calls to _bt_advance_array_keys from
_bt_check_compare (that won't just happen for the scan's SAOP arrays
in v24). This approach seems clearer to me. More importantly, it
performs much better than the previous approach in certain complicated
cases involving multiple range skip arrays against several different
index columns. (I can elaborate on what those queries look like and
why they're better with this new approach, if anybody wants me to.)

--
Peter Geoghegan

Attachments:

v24-0006-DEBUG-Add-skip-scan-disable-GUCs.patchapplication/x-patch; name=v24-0006-DEBUG-Add-skip-scan-disable-GUCs.patchDownload
From 5ec93271364664ccce246fc4b117d5164e778776 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 18 Jan 2025 10:54:44 -0500
Subject: [PATCH v24 6/6] DEBUG: Add skip scan disable GUCs.

---
 src/include/access/nbtree.h                   |  4 +++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 31 +++++++++++++++++++
 src/backend/utils/misc/guc_tables.c           | 23 ++++++++++++++
 3 files changed, 58 insertions(+)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index edb06f8eb..6a26e4c9b 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1184,6 +1184,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index cb7b8be2b..bddbb59ea 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -21,6 +21,27 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 typedef struct BTScanKeyPreproc
 {
 	ScanKey		inkey;
@@ -1634,6 +1655,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
 			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
 
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].sksup = NULL;
+
 			/*
 			 * We'll need a 3-way ORDER proc to perform "binary searches" for
 			 * the next matching array element.  Set that up now.
@@ -2139,6 +2164,12 @@ _bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
 		if (attno_has_rowcompare)
 			break;
 
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
 		/*
 		 * Now consider next attno_inkey (or keep going if this is an
 		 * additional scan key against the same attribute)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 427281893..4c10f7895 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1772,6 +1773,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3637,6 +3649,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
-- 
2.47.2

v24-0004-Lower-the-overhead-of-nbtree-runtime-skip-checks.patchapplication/x-patch; name=v24-0004-Lower-the-overhead-of-nbtree-runtime-skip-checks.patchDownload
From 6e76320446b0e53687e169f1f80b29f87b3abdac Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v24 4/6] Lower the overhead of nbtree runtime skip checks.

Add a new optimization that fixes regressions in index scans that are
nominally eligible to use skip scan, but can never actually benefit from
skipping.  These are cases where a leading prefix column contains many
distinct values -- especially when the number of values approaches the
total number of index tuples, where skipping cannot possibly help.

The optimization is activated dynamically, as our fall back strategy.
It works by determining a prefix of leading index columns whose scan
keys (often skip array scan keys) are guaranteed to be satisfied by
every possible index tuple from a given page.  _bt_readpage is then able
to start comparisons at the first scan key that might not be satisfied.
This necessitates making _bt_readpage temporarily cease maintaining the
scan's arrays during _bt_checkkeys calls prior to the finaltup call
(_bt_checkkeys treats the scan's keys as nonrequired, including both
SAOP arrays and skip arrays).

This optimization has no impact on primitive index scan scheduling.  It
is similar to the precheck optimizations added by commit e0b1ee17dc,
though it is only used during nbtree index scans that use skip arrays.
The optimization can even improve performance with certain queries that
use inequality range skip arrays -- even when there is no benefit from
skipping.  This is possible wherever the new optimization lowers the
absolute number of scan key comparisons.

Skip scan's approach of adding skip arrays during preprocessing, and
fixing (or significantly ameliorating) the resulting regressions in
unsympathetic cases is enabled by the optimization added by this commit
(and by the "look ahead" array optimization added by commit 5bf748b8).
This enables skip scan's approach within the planner: we don't create
distinct index paths to represent nbtree index skip scans.  Whether and
to what extent a scan actually skips is always determined dynamically,
at runtime.  This makes affected nbtree index scans robust against data
skew, cardinality estimation errors, and cost estimation errors.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h           |   5 +-
 src/backend/access/nbtree/nbtsearch.c |  36 +++
 src/backend/access/nbtree/nbtutils.c  | 338 +++++++++++++++++++++++---
 3 files changed, 339 insertions(+), 40 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index dc664d344..edb06f8eb 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1115,11 +1115,13 @@ typedef struct BTReadPageState
 
 	/*
 	 * Input and output parameters, set and unset by both _bt_readpage and
-	 * _bt_checkkeys to manage precheck optimizations
+	 * _bt_checkkeys to manage precheck and forcenonrequired optimizations
 	 */
 	bool		firstpage;		/* on first page of current primitive scan? */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
+	bool		forcenonrequired;	/* treat all scan keys as nonrequired? */
+	int			ikey;			/* start comparisons from ikey'th scan key */
 
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
@@ -1328,6 +1330,7 @@ extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arra
 						  IndexTuple tuple, int tupnatts);
 extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
 									 IndexTuple finaltup);
+extern void _bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 01821486b..51c363c2e 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1650,6 +1650,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.firstpage = firstPage;
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
+	pstate.forcenonrequired = false;
+	pstate.ikey = 0;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
@@ -1730,6 +1732,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 				}
 			}
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
+
+			/*
+			 * Skip maintenance of skip arrays (if any) during primitive index
+			 * scans that read leaf pages after the first
+			 */
+			if (!pstate.firstpage && so->skipScan && minoff < maxoff)
+				_bt_skip_ikeyprefix(scan, &pstate);
 		}
 
 		/* load items[] in ascending order */
@@ -1768,6 +1777,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum < pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1833,6 +1843,15 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
+			if (pstate.forcenonrequired)
+			{
+				Assert(so->skipScan);
+
+				/* recover from treating the scan's keys as nonrequired */
+				_bt_start_array_keys(scan, dir);
+				pstate.forcenonrequired = false;
+				pstate.ikey = 0;
+			}
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -1865,6 +1884,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 				}
 			}
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
+
+			/*
+			 * Skip maintenance of skip arrays (if any) during primitive index
+			 * scans that read leaf pages after the first
+			 */
+			if (!pstate.firstpage && so->skipScan && minoff < maxoff)
+				_bt_skip_ikeyprefix(scan, &pstate);
 		}
 
 		/* load items[] in descending order */
@@ -1906,6 +1932,15 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff && pstate.forcenonrequired)
+			{
+				Assert(so->skipScan);
+
+				/* recover from treating the scan's keys as nonrequired */
+				_bt_start_array_keys(scan, dir);
+				pstate.forcenonrequired = false;
+				pstate.ikey = 0;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
@@ -1917,6 +1952,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum > pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 1c1ac1ec9..37d421f45 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -57,11 +57,12 @@ static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool forcenonrequired,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-								 ScanDirection dir, bool *continuescan);
+								 ScanDirection dir, bool forcenonrequired, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -1383,14 +1384,14 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
  * postcondition's <= operator with a >=.  In other words, just swap the
  * precondition with the postcondition.)
  *
- * We also deal with "advancing" non-required arrays here.  Callers whose
- * sktrig scan key is non-required specify sktrig_required=false.  These calls
- * are the only exception to the general rule about always advancing the
+ * We also deal with "advancing" non-required arrays here (or arrays that are
+ * treated as non-required for the duration of a _bt_readpage call).  Callers
+ * whose sktrig scan key is non-required specify sktrig_required=false.  These
+ * calls are the only exception to the general rule about always advancing the
  * required array keys (the scan may not even have a required array).  These
  * callers should just pass a NULL pstate (since there is never any question
  * of stopping the scan).  No call to _bt_tuple_before_array_skeys is required
- * ahead of these calls (it's already clear that any required scan keys must
- * be satisfied by caller's tuple).
+ * ahead of these calls.
  *
  * Note that we deal with non-array required equality strategy scan keys as
  * degenerate single element arrays here.  Obviously, they can never really
@@ -1443,8 +1444,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		pstate->firstmatch = false;
 
-		/* Shouldn't have to invalidate 'prechecked', though */
-		Assert(!pstate->prechecked);
+		/* Shouldn't have to invalidate precheck/forcenonrequired state */
+		Assert(!pstate->prechecked && !pstate->forcenonrequired &&
+			   pstate->ikey == 0);
 
 		/*
 		 * Once we return we'll have a new set of required array keys, so
@@ -1453,6 +1455,27 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		pstate->rechecks = 0;
 		pstate->targetdistance = 0;
 	}
+	else if (so->skipScan && sktrig < so->numberOfKeys - 1)
+	{
+		int			lowikey = so->numberOfKeys - 1;
+		ScanKey		trig = so->keyData + sktrig;
+		ScanKey		low = so->keyData + lowikey;
+
+		/*
+		 * "nonrequired" skip scan call optimization: perform a precheck of
+		 * the least significant key to avoid array maintenance overhead
+		 * (though only if it isn't a SAOP array).
+		 */
+		if (!(trig->sk_flags & SK_ISNULL) && !(low->sk_flags & SK_SEARCHARRAY))
+		{
+			bool		continuescan;
+
+			if (!_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false,
+								   false, false, false, &continuescan,
+								   &lowikey))
+				return false;
+		}
+	}
 
 	Assert(_bt_verify_keys_with_arraykeys(scan));
 
@@ -1496,8 +1519,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -1643,7 +1664,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -1685,7 +1706,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -1700,7 +1721,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -1760,6 +1781,12 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * of any required scan key).  All that matters is whether caller's tuple
 	 * satisfies the new qual, so it's safe to just skip the _bt_check_compare
 	 * recheck when we've already determined that it can only return 'false'.
+	 *
+	 * Note: In practice most scan keys are marked required by preprocessing,
+	 * if necessary by generating a preceding skip array.  We nevertheless
+	 * often handle array keys marked required as if they were nonrequired.
+	 * This behavior is requested by our _bt_check_compare caller, though only
+	 * when it is passed "advancenonrequired=true" + "forcenonrequired=true".
 	 */
 	if ((sktrig_required && all_required_satisfied) ||
 		(!sktrig_required && all_satisfied))
@@ -1771,7 +1798,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -2030,8 +2057,10 @@ new_prim_scan:
 	 * the scan arrives on the next sibling leaf page) when it has already
 	 * read at least one leaf page before the one we're reading now.  This is
 	 * important when reading subsets of an index with many distinct values in
-	 * respect of an attribute constrained by an array.  It encourages fewer,
-	 * larger primitive scans where that makes sense.
+	 * respect of an attribute constrained by an array (often a skip array).
+	 * It encourages fewer, larger primitive scans where that makes sense.
+	 * This will in turn encourage _bt_readpage to apply the forcenonrequired
+	 * optimization when applicable (i.e. when the scan has a skip array).
 	 *
 	 * Note: This heuristic isn't as aggressive as you might think.  We're
 	 * conservative about allowing a primitive scan to step from the first
@@ -2215,14 +2244,15 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	TupleDesc	tupdesc = RelationGetDescr(scan->indexRelation);
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ScanDirection dir = so->currPos.dir;
-	int			ikey = 0;
+	int			ikey = pstate->ikey;
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 	Assert(!so->scanBehind && !so->oppositeDirCheck);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+							arrayKeys, pstate->forcenonrequired,
+							pstate->prechecked, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
@@ -2233,22 +2263,23 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 *
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
-		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
+		Assert(!pstate->prechecked && !pstate->firstmatch &&
+			   !pstate->forcenonrequired);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
 	if (pstate->prechecked || pstate->firstmatch)
 	{
 		bool		dcontinuescan;
-		int			dikey = 0;
+		int			dikey = pstate->ikey;
 
 		/*
 		 * Call relied on continuescan/firstmatch prechecks -- assert that we
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, pstate->forcenonrequired,
+										false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -2271,6 +2302,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * It's also possible that the scan is still _before_ the _start_ of
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
+	Assert(!pstate->forcenonrequired);
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
@@ -2386,7 +2418,7 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	Assert(so->numArrayKeys);
 
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -2394,6 +2426,195 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	return true;
 }
 
+/*
+ * Determine a prefix of scan keys that are guaranteed to be satisfied by
+ * every possible tuple on pstate's page.  Used during scans with skip arrays,
+ * which do not use the similar optimization controlled by pstate.prechecked.
+ *
+ * Sets pstate.ikey (and pstate.forcenonrequired) on success, making later
+ * calls to _bt_checkkeys start checks of each tuple from the so->keyData[]
+ * entry at pstate.ikey (while treating keys >= pstate.ikey as nonrequired).
+ *
+ * When _bt_checkkeys treats the scan's required keys as non-required, the
+ * scan's array keys won't be properly maintained (they won't have advanced in
+ * lockstep with our progress through the index's key space as expected).
+ * Caller must recover from this by restarting the scan's array keys and
+ * resetting pstate.ikey and pstate.forcenonrequired just ahead of the
+ * _bt_checkkeys call for the page's final tuple (the pstate.finaltup tuple).
+ */
+void
+_bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	ItemId		iid;
+	IndexTuple	firsttup,
+				lasttup;
+	int			ikey = 0,
+				arrayidx = 0,
+				firstchangingattnum;
+
+	Assert(so->skipScan && pstate->minoff < pstate->maxoff);
+
+	/* minoff is an offset to the lowest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->minoff);
+	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* maxoff is an offset to the highest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->maxoff);
+	lasttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* Determine the first attribute whose values change on caller's page */
+	firstchangingattnum = _bt_keep_natts_fast(rel, firsttup, lasttup);
+
+	for (; ikey < so->numberOfKeys; ikey++)
+	{
+		ScanKey		cur = so->keyData + ikey;
+		BTArrayKeyInfo *array;
+		Datum		tupdatum;
+		bool		tupnull;
+		int32		result;
+
+		/*
+		 * Determine if it's safe to set pstate.ikey to an offset to a key
+		 * that comes after this key, by examining this key
+		 */
+		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) == 0)
+		{
+			/*
+			 * This is the first key that is not marked required (only happens
+			 * in corner cases where _bt_preprocess_keys couldn't mark all
+			 * keys required due to restrictions on generating skip arrays)
+			 */
+			Assert(!(cur->sk_flags & SK_BT_SKIP));
+			break;				/* pstate.ikey to be set to nonrequired ikey */
+		}
+		if (cur->sk_strategy != BTEqualStrategyNumber)
+		{
+			/*
+			 * This is the scan's first inequality key (barring inequalities
+			 * that are used to describe the range of a = skip array key).
+			 *
+			 * We could handle this like a = key, but it doesn't seem worth
+			 * the trouble.  Have _bt_checkkeys start with this inequality.
+			 */
+			break;				/* pstate.ikey to be set to inequality's ikey */
+		}
+		if (!(cur->sk_flags & SK_SEARCHARRAY))
+		{
+			/*
+			 * Found a scalar (non-array) = key.
+			 *
+			 * It is unsafe to set pstate.ikey to an ikey beyond this key,
+			 * unless the = key is satisfied by every possible tuple on the
+			 * page (possible only when attribute has just one distinct value
+			 * among all tuples on the page).
+			 */
+			if (cur->sk_attno < firstchangingattnum)
+			{
+				/*
+				 * Only one distinct value on the page for this key's column.
+				 * Must make sure that = key is actually satisfied by the
+				 * value that is stored within every tuple on the page.
+				 */
+				tupdatum = index_getattr(firsttup, cur->sk_attno, tupdesc,
+										 &tupnull);
+				result = _bt_compare_array_skey(&so->orderProcs[ikey],
+												tupdatum, tupnull,
+												cur->sk_argument, cur);
+				if (result == 0)
+					continue;	/* safe, = key satisfied by every tuple */
+			}
+			break;				/* pstate.ikey to be set to scalar key's ikey */
+		}
+
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == ikey);
+		if (array->num_elems != -1)
+		{
+			/*
+			 * Found a SAOP array = key.
+			 *
+			 * Handle this just like we handle scalar = keys.
+			 */
+			if (cur->sk_attno < firstchangingattnum)
+			{
+				/*
+				 * Only one distinct value on the page for this key's column.
+				 * Must make sure that SAOP array is actually satisfied by the
+				 * value that is stored within every tuple on the page.
+				 */
+				tupdatum = index_getattr(firsttup, cur->sk_attno, tupdesc,
+										 &tupnull);
+				_bt_binsrch_array_skey(&so->orderProcs[ikey], false,
+									   NoMovementScanDirection,
+									   tupdatum, tupnull, array, cur, &result);
+				if (result == 0)
+					continue;	/* safe, SAOP = key satisfied by every tuple */
+			}
+			break;				/* pstate.ikey to be set to SAOP array's ikey */
+		}
+
+		/*
+		 * Found a skip array = key.
+		 *
+		 * As with other = keys, moving past this skip array key is safe when
+		 * every tuple on the page is guaranteed to satisfy the array's key.
+		 * But we need a slightly different approach, since skip arrays make
+		 * it easy to assess whether all the values on the page fall within
+		 * the skip array's entire range.
+		 */
+		if (array->null_elem)
+			continue;			/* safe, non-range skip array "satisfied" */
+		else if (cur->sk_attno > firstchangingattnum)
+		{
+			/*
+			 * We cannot assess whether this range skip array will definitely
+			 * be satisfied by every tuple on the page, since its attribute is
+			 * preceded by another attribute that does not contain the same
+			 * prefix of value(s) within every tuple from caller's page
+			 */
+			break;				/* pstate.ikey to be set to range array's ikey */
+		}
+
+		/*
+		 * Found a range skip array = key.
+		 *
+		 * It's definitely safe for _bt_checkkeys to avoid assessing this
+		 * range skip array when the page's first and last non-pivot tuples
+		 * both satisfy the range skip array (since the same must also be true
+		 * of all the tuples in between these two).
+		 */
+		tupdatum = index_getattr(firsttup, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(true, BackwardScanDirection,
+								   tupdatum, tupnull, array, cur, &result);
+		if (result != 0)
+			break;				/* pstate.ikey to be set to range array's ikey */
+
+		tupdatum = index_getattr(lasttup, cur->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(true, ForwardScanDirection,
+								   tupdatum, tupnull, array, cur, &result);
+		if (result != 0)
+			break;				/* pstate.ikey to be set to range array's ikey */
+
+		/* safe, range skip array satisfied by every tuple on page */
+	}
+
+	/*
+	 * We always instruct _bt_checkkeys (and our _bt_readpage caller) to
+	 * temporarily treat the scan's keys as nonrequired, even in rare cases
+	 * where that isn't needed to make it safe to use a non-zero pstate.ikey.
+	 *
+	 * There is still some advantage to this.  _bt_advance_array_keys will
+	 * still apply an optimization for callers whose sktrig is a "nonrequired"
+	 * skip array key (it performs a precheck of the least significant key,
+	 * which avoids array maintenance overhead).
+	 */
+	pstate->ikey = ikey;
+	pstate->forcenonrequired = true;
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
@@ -2425,17 +2646,25 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Though we advance non-required array keys on our own, that shouldn't have
  * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
+ * have no fixed relationship with the scan's progress.
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass forcenonrequired=true to instruct us to treat all keys as nonrequried.
+ * This is used to make it safe to temporarily stop properly maintaining the
+ * scan's required arrays.  Callers can determine which prefix of keys must
+ * satisfy every possible prefix of index attribute values on the page, and
+ * then pass us an initial *ikey for the first key that might be unsatisified.
+ * We won't be maintaining any arrays before that initial *ikey, so there is
+ * no point in trying to do so for any later arrays.  (Callers that do this
+ * must be careful to reset the array keys when they finish reading the page.)
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool forcenonrequired,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -2452,10 +2681,13 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when we're forced to treat all scan keys as nonrequired)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (forcenonrequired)
+			Assert(!prechecked);
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
@@ -2503,7 +2735,16 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		{
 			Assert(key->sk_flags & SK_SEARCHARRAY);
 			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir || forcenonrequired);
 
+			/*
+			 * Cannot fall back on _bt_tuple_before_array_skeys when we're
+			 * treating the scan's keys as nonrequired, though.  Just handle
+			 * this like any other non-required equality-type array key.
+			 */
+			if (!requiredSameDir && advancenonrequired)
+				return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
+											  tupdesc, *ikey, false);
 			*continuescan = false;
 			return false;
 		}
@@ -2512,7 +2753,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
-									 continuescan))
+									 forcenonrequired, continuescan))
 				continue;
 			return false;
 		}
@@ -2541,9 +2782,17 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			 * Tuple fails this qual.  If it's a required qual for the current
 			 * scan direction, then we can conclude no further tuples will
 			 * pass, either.
+			 *
+			 * It's possible that this is a skip array whose current element
+			 * IS NULL.  If we're treating scan keys as required, then caller
+			 * will consider array advancement when we return.  Otherwise we
+			 * have to consider advancing the arrays directly.
 			 */
 			if (requiredSameDir)
 				*continuescan = false;
+			else if (advancenonrequired && (key->sk_flags & SK_BT_SKIP))
+				return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
+											  tupdesc, *ikey, false);
 
 			/*
 			 * In any case, this indextuple doesn't match the qual.
@@ -2567,7 +2816,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -2585,7 +2834,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -2654,7 +2903,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
  */
 static bool
 _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
-					 TupleDesc tupdesc, ScanDirection dir, bool *continuescan)
+					 TupleDesc tupdesc, ScanDirection dir,
+					 bool forcenonrequired, bool *continuescan)
 {
 	ScanKey		subkey = (ScanKey) DatumGetPointer(skey->sk_argument);
 	int32		cmpresult = 0;
@@ -2694,7 +2944,11 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 
 		if (isNull)
 		{
-			if (subkey->sk_flags & SK_BT_NULLS_FIRST)
+			if (forcenonrequired)
+			{
+				/* treating scan key as non-required */
+			}
+			else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -2748,8 +3002,12 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			 */
 			Assert(subkey != (ScanKey) DatumGetPointer(skey->sk_argument));
 			subkey--;
-			if ((subkey->sk_flags & SK_BT_REQFWD) &&
-				ScanDirectionIsForward(dir))
+			if (forcenonrequired)
+			{
+				/* treating scan key as non-required */
+			}
+			else if ((subkey->sk_flags & SK_BT_REQFWD) &&
+					 ScanDirectionIsForward(dir))
 				*continuescan = false;
 			else if ((subkey->sk_flags & SK_BT_REQBKWD) &&
 					 ScanDirectionIsBackward(dir))
@@ -2801,7 +3059,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			break;
 	}
 
-	if (!result)
+	if (!result && !forcenonrequired)
 	{
 		/*
 		 * Tuple fails this qual.  If it's a required qual for the current
@@ -2845,6 +3103,8 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	Assert(!pstate->forcenonrequired);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
-- 
2.47.2

v24-0005-Apply-low-order-skip-key-in-_bt_first-more-often.patchapplication/x-patch; name=v24-0005-Apply-low-order-skip-key-in-_bt_first-more-often.patchDownload
From ae42343a6b88194d9ead7346ef754ed8033a0914 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 13 Jan 2025 16:08:32 -0500
Subject: [PATCH v24 5/6] Apply low-order skip key in _bt_first more often.

Convert low_compare and high_compare nbtree skip array inequalities
(with opclasses that offer skip support) such that _bt_first will always
be able to use any low-order keys in _bt_first.

For example, an index qual "WHERE a > 5 AND b = 2" is now converted to
"WHERE a >= 6 AND b = 2" by a new preprocessing step that takes place
after a final low_compare and/or high_compare are chosen by all earlier
preprocessing steps.  That way the scan's initial call to _bt_first will
use "WHERE a >= 6 AND b = 2" to find the initial leaf level position,
rather than merely using "WHERE a > 5" ("b = 2" can always be included).
In practice this transformation has a decent chance of enabling nbtree
to avoid an extra _bt_first call just to determine the lowest-sorting
"a" value in the index (the lowest that still satisfies "WHERE a > 5").

This transformation process can only lower the total number of index
pages read when the use of a more restrictive set of initial positioning
keys in _bt_first actually allows the scan to land on some later leaf
page directly, relative to the unoptimized case (or on an earlier leaf
page directly, when we're scanning backwards).  The savings can be far
greater when affected skip arrays come after some higher-order array.
For example, a qual "WHERE x IN (1, 2, 3) AND y > 5 AND z = 2" can now
save as many as 3 _bt_first calls as a result of these transformations
(there can be as many as 1 _bt_first call saved per "x" array element).

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=FJ78K3WsF3iWNxWnUCY9f=Jdg3QPxaXE=uYUbmuRz5Q@mail.gmail.com
---
 src/backend/access/nbtree/nbtpreprocesskeys.c | 175 ++++++++++++++++++
 src/test/regress/expected/create_index.out    |  21 +++
 src/test/regress/sql/create_index.sql         |  10 +
 3 files changed, 206 insertions(+)

diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 540a146a1..cb7b8be2b 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -50,6 +50,12 @@ static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
 								 BTArrayKeyInfo *array, bool *qual_ok);
 static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
 								 BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+									   BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
@@ -1286,6 +1292,166 @@ _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
 	return true;
 }
 
+/*
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts their
+ * operator strategies, while changing the operator as appropriate).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value MINVAL, using a transformed >= key
+ * instead of using the original > key makes it safe to include lower-order
+ * scan keys in the insertion scan key (there must be lower-order scan keys
+ * after the skip array).  We will avoid an extra _bt_first to find the first
+ * value in the index > sk_argument -- at least when the first real matching
+ * value in the index happens to be an exact match for the sk_argument value
+ * that we produced here by incrementing the original input key's sk_argument.
+ * (Backwards scans derive the same benefit when they encounter the sentinel
+ * value MAXVAL, by converting the high_compare key from < to <=.)
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01'.
+ */
+static void
+_bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+						   BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	/*
+	 * Called last among all preprocessing steps, when the skip array's final
+	 * low_compare and high_compare have both been chosen
+	 */
+	Assert(so->skipScan);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1 && !array->null_elem && array->sksup);
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skiparray_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skiparray_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1822,6 +1988,15 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && array->sksup &&
+						!array->null_elem)
+						_bt_skiparray_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 113fc3293..4488fa45b 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2559,6 +2559,27 @@ ORDER BY thousand;
         1 |     1001
 (1 row)
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+                                            QUERY PLAN                                            
+--------------------------------------------------------------------------------------------------
+ Limit
+   ->  Index Only Scan using tenk1_thous_tenthous on tenk1
+         Index Cond: ((thousand > '-1'::integer) AND (tenthous = ANY ('{1001,3000}'::integer[])))
+(3 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+        1 |     1001
+(2 rows)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index bb4782cae..ef9639be9 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -987,6 +987,16 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
 ORDER BY thousand;
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
-- 
2.47.2

v24-0003-Add-nbtree-skip-scan-optimizations.patchapplication/x-patch; name=v24-0003-Add-nbtree-skip-scan-optimizations.patchDownload
From 5a964fe814024e6f3aec8c5b901c6c6fb605c761 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v24 3/6] Add nbtree skip scan optimizations.

Teach nbtree composite index scans to opportunistically skip over
irrelevant sections of the index.  This is possible whenever the scan
encounters a group of tuples that all share a common prefix of the same
index column value.  Each such group of tuples can be thought of as a
"logical subindex".  The scan exhaustively "searches every subindex".
Scans of composite indexes with a leading low cardinality column can now
skip over irrelevant sections of the index, which is far more efficient.

When nbtree is passed input scan keys derived from a query predicate
"WHERE b = 5", new nbtree preprocessing steps now output scan keys for
"WHERE a = ANY(<every possible 'a' value>) AND b = 5".  That is, nbtree
preprocessing generates a "skip array" (and an associated scan key) for
the omitted column "a", thereby enabling marking the scan key on "b" as
required to continue the scan.  This approach builds on the design for
ScalarArrayOp scans established by commit 5bf748b8: skip arrays generate
their values procedurally, but otherwise work just like SAOP arrays.

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values behave just like any other
value that might appear in an array -- though they can never actually
locate matching index tuples.

Inequality scan keys can affect how skip arrays generate their values.
Their range is constrained by the inequalities.  For example, a skip
array on "a" will only ever use element values 1 and 2 given a qual
"WHERE a BETWEEN 1 AND 2 AND b = 66".  A scan using such a skip array
has almost identical performance characteristics to a scan with the qual
"WHERE a = ANY('{1, 2}') AND b = 66".  The scan will be much faster when
it can be executed as two highly selective primitive index scans, rather
than a single much larger scan that has to read many more leaf pages.
It isn't guaranteed to improve performance, though.

B-Tree preprocessing is optimistic about skipping working out: it
applies simple generic rules to determine where to generate skip arrays.
This assumes that the runtime overhead of maintaining skip arrays will
pay for itself, or at worst lead to only a modest loss in performance.
As things stand, our assumptions about possible downsides are much too
optimistic: skip array maintenance will lead to regressions that are
clearly unacceptable with unsympathetic queries (typically queries where
the fastest plan necessitates a traditional full index scan, with little
to no potential for the scan to skip over irrelevant index leaf pages).
A pending commit will ameliorate the problems in this area by making
affected scans temporarily disable skip array maintenance for a page.
It seems natural to commit this separately, since the break-even point
at which the scan should favor skipping (or favor sequentially reading
every index tuple, without the use of any extra skip array keys) is the
single most subtle aspect of the work in this area.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |   3 +-
 src/include/access/nbtree.h                   |  35 +-
 src/include/catalog/pg_amproc.dat             |  22 +
 src/include/catalog/pg_proc.dat               |  27 +
 src/include/storage/lwlock.h                  |   1 +
 src/include/utils/skipsupport.h               |  97 +++
 src/backend/access/index/indexam.c            |   3 +-
 src/backend/access/nbtree/nbtcompare.c        | 273 +++++++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 599 +++++++++++++--
 src/backend/access/nbtree/nbtree.c            | 211 +++++-
 src/backend/access/nbtree/nbtsearch.c         | 111 ++-
 src/backend/access/nbtree/nbtutils.c          | 712 ++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |   4 +
 src/backend/commands/opclasscmds.c            |  25 +
 src/backend/storage/lmgr/lwlock.c             |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/date.c                  |  46 ++
 src/backend/utils/adt/meson.build             |   1 +
 src/backend/utils/adt/selfuncs.c              | 405 +++++++---
 src/backend/utils/adt/skipsupport.c           |  61 ++
 src/backend/utils/adt/timestamp.c             |  48 ++
 src/backend/utils/adt/uuid.c                  |  70 ++
 doc/src/sgml/btree.sgml                       |  34 +-
 doc/src/sgml/indexam.sgml                     |   3 +-
 doc/src/sgml/indices.sgml                     |  49 +-
 doc/src/sgml/xindex.sgml                      |  16 +-
 src/test/regress/expected/alter_generic.out   |  10 +-
 src/test/regress/expected/btree_index.out     |  41 +
 src/test/regress/expected/create_index.out    | 103 ++-
 src/test/regress/expected/psql.out            |   3 +-
 src/test/regress/sql/alter_generic.sql        |   5 +-
 src/test/regress/sql/btree_index.sql          |  21 +
 src/test/regress/sql/create_index.sql         |  51 +-
 src/tools/pgindent/typedefs.list              |   3 +
 35 files changed, 2759 insertions(+), 337 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index 6723de75a..a718b864f 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -214,7 +214,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 599e6fa4c..dc664d344 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -707,6 +708,10 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
  *	(BTOPTIONS_PROC).  These procedures define a set of user-visible
  *	parameters that can be used to control operator class behavior.  None of
  *	the built-in B-Tree operator classes currently register an "options" proc.
+ *
+ *	To facilitate more efficient B-Tree skip scans, an operator class may
+ *	choose to offer a sixth amproc procedure (BTSKIPSUPPORT_PROC).  For full
+ *	details, see src/include/utils/skipsupport.h.
  */
 
 #define BTORDER_PROC		1
@@ -714,7 +719,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1027,10 +1033,21 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
-	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+	int			cur_elem;		/* index of current element in elem_values */
+
+	/* fields for skip arrays, which generate element datums procedurally */
+	int16		attlen;			/* attr len in bytes */
+	bool		attbyval;		/* as FormData_pg_attribute.attbyval */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupport sksup;			/* skip support (NULL if opclass lacks it) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1042,6 +1059,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* arrays might be ahead of scan's key space? */
 	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
@@ -1119,6 +1137,15 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on column without input = */
+
+/* SK_BT_SKIP-only flags (mutable, set and unset by array advancement) */
+#define SK_BT_MINVAL	0x00080000	/* invalid sk_argument, use low_compare */
+#define SK_BT_MAXVAL	0x00100000	/* invalid sk_argument, use high_compare */
+#define SK_BT_NEXT		0x00200000	/* positions the scan > sk_argument */
+#define SK_BT_PRIOR		0x00400000	/* positions the scan < sk_argument */
+
+/* Remaps pg_index flag bits to uppermost SK_BT_* byte */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1165,7 +1192,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 508f48d34..e9194e68e 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9e803d610..8a7d9f541 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2266,6 +2281,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4456,6 +4474,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6328,6 +6349,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9376,6 +9400,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 2aa46fd50..6803d5b9e 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -192,6 +192,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..94389ead2
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,97 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining groups of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * The B-Tree code falls back on next-key sentinel values for any opclass that
+ * doesn't provide its own skip support function.  There is no benefit to
+ * providing skip support for an opclass on a type where guessing that the
+ * next indexed value is the next possible indexable value seldom works out.
+ * It might not even be feasible to add skip support for a continuous type.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order)
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 * Operator class decrement/increment functions will never be called with
+	 * a NULL "existing" argument, either.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern SkipSupport PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+												 bool reverse);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 8b1f55543..0e62d7a7a 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 291cb8fc1..4da5a3c1d 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 1fd1da5f1..540a146a1 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -45,8 +45,15 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
+static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
+								 ScanKey skey, FmgrInfo *orderproc,
+								 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
+								 BTArrayKeyInfo *array, bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
+							   int *numSkipArrayKeys);
 static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
 									  Oid elemtype, StrategyNumber strat,
 									  Datum *elems, int nelems);
@@ -89,6 +96,8 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -101,10 +110,16 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never generated skip array scan keys, it would be possible for "gaps"
+ * to appear that make it unsafe to mark any subsequent input scan keys
+ * (copied from scan->keyData[]) as required to continue the scan.  Prior to
+ * Postgres 18, a qual like "WHERE y = 4" always required a full index scan.
+ * It'll now become "WHERE x = ANY('{every possible x value}') and y = 4" on
+ * output, due to the addition of a skip array on "x".  This has the potential
+ * to be much more efficient than a full index scan (though it'll behave like
+ * a full index scan when there are many distinct values for "x").
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -141,7 +156,12 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -200,6 +220,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProcs[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -231,6 +259,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
+		Assert(!so->skipScan);
 
 		return;
 	}
@@ -288,7 +317,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -345,7 +375,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -393,6 +422,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, _bt_preprocess_array_keys has already added "=" keys
+			 * sufficient to form an unbroken series of "=" constraints on all
+			 * attrs prior to the attr from the final scan->keyData[] key.
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -481,6 +515,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -489,6 +524,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -508,8 +544,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -803,6 +837,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -812,6 +849,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -885,6 +938,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -978,24 +1032,55 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
  * Compare an array scan key to a scalar scan key, eliminating contradictory
  * array elements such that the scalar scan key becomes redundant.
  *
+ * If the opfamily is incomplete we may not be able to determine which
+ * elements are contradictory.  When we return true we'll have validly set
+ * *qual_ok, guaranteeing that at least the scalar scan key can be considered
+ * redundant.  We return false if the comparison could not be made (caller
+ * must keep both scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar scan keys.
+ */
+static bool
+_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
+							   bool *qual_ok)
+{
+	Assert(arraysk->sk_attno == skey->sk_attno);
+	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
+		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
+	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
+		   skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Just call the appropriate helper function based on whether it's a SAOP
+	 * array or a skip array.  Both helpers will set *qual_ok in passing.
+	 */
+	if (array->num_elems != -1)
+		return _bt_saoparray_shrink(scan, arraysk, skey, orderproc, array,
+									qual_ok);
+	else
+		return _bt_skiparray_shrink(scan, skey, array, qual_ok);
+}
+
+/*
+ * Preprocessing of SAOP (non-skip) array scan key, used to determine which
+ * array elements are eliminated as contradictory by a non-array scalar key.
+ * _bt_compare_array_scankey_args helper function.
+ *
  * Array elements can be eliminated as contradictory when excluded by some
  * other operator on the same attribute.  For example, with an index scan qual
  * "WHERE a IN (1, 2, 3) AND a < 2", all array elements except the value "1"
  * are eliminated, and the < scan key is eliminated as redundant.  Cases where
  * every array element is eliminated by a redundant scalar scan key have an
  * unsatisfiable qual, which we handle by setting *qual_ok=false for caller.
- *
- * If the opfamily doesn't supply a complete set of cross-type ORDER procs we
- * may not be able to determine which elements are contradictory.  If we have
- * the required ORDER proc then we return true (and validly set *qual_ok),
- * guaranteeing that at least the scalar scan key can be considered redundant.
- * We return false if the comparison could not be made (caller must keep both
- * scan keys when this happens).
  */
 static bool
-_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
-							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
-							   bool *qual_ok)
+_bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+					 FmgrInfo *orderproc, BTArrayKeyInfo *array, bool *qual_ok)
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
@@ -1006,14 +1091,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
-	Assert(arraysk->sk_attno == skey->sk_attno);
 	Assert(array->num_elems > 0);
-	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
-		   arraysk->sk_strategy == BTEqualStrategyNumber);
-	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
-		   skey->sk_strategy != BTEqualStrategyNumber);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
 
 	/*
 	 * _bt_binsrch_array_skey searches an array for the entry best matching a
@@ -1112,6 +1191,101 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	return true;
 }
 
+/*
+ * Preprocessing of skip (non-SAOP) array scan key, used to determine
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function.
+ *
+ * Unlike _bt_saoparray_shrink, we don't modify caller's array in-place.  Skip
+ * arrays work by procedurally generating their elements as needed, so we just
+ * store the inequality as the skip array's low_compare or high_compare.  The
+ * array's elements will be generated from the range of values that satisfies
+ * both low_compare and high_compare.
+ */
+static bool
+_bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
+					 bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Array's index attribute will be constrained by a strict operator/key.
+	 * Array must not "contain a NULL element" (i.e. the scan must not apply
+	 * "IS NULL" qual when it reaches the end of the index that stores NULLs).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Consider if we should treat caller's scalar scan key as the skip
+	 * array's high_compare or low_compare.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way the scan can always safely assume that
+	 * it's okay to use the input-opclass-only-type proc from so->orderProcs[]
+	 * (they can be cross-type with a SAOP array, but never with skip arrays).
+	 *
+	 * This approach is enabled by MINVAL/MAXVAL sentinel key markings, which
+	 * can be thought of as representing either the lowest or highest matching
+	 * array element.  An = array key marked MINVAL/MAXVAL never has a valid
+	 * datum stored in its sk_argument.  The scan must directly apply the
+	 * array's low_compare when it encounters MINVAL (or its high_compare when
+	 * it encounters MAXVAL), and must never use array's so->orderProcs[] proc
+	 * against low_compare's/high_compare's sk_argument.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* replace existing high_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing high_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's high_compare */
+			array->high_compare = skey;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* replace existing low_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing low_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's low_compare */
+			array->low_compare = skey;
+			break;
+		case BTEqualStrategyNumber:
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1137,6 +1311,12 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -1152,41 +1332,36 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	Oid			skip_eq_ops[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
-	ScanKey		cur;
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
+	so->skipScan = (numSkipArrayKeys > 0);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys we'll need to generate below
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -1201,18 +1376,20 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
+		ScanKey		inkey = scan->keyData + input_ikey,
+					cur;
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
 		Oid			elemtype;
@@ -1225,21 +1402,114 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		Datum	   *elem_values;
 		bool	   *elem_nulls;
 		int			num_nonnulls;
-		int			j;
+
+		/* set up next output scan key */
+		cur = &arrayKeyData[numArrayKeyData];
+
+		/* Backfill skip arrays for attrs < or <= input key's attr? */
+		while (numSkipArrayKeys && attno_skip <= inkey->sk_attno)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skip_eq_ops[attno_skip - 1];
+			CompactAttribute *attr;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/*
+				 * Attribute already has an = input key, so don't output a
+				 * skip array for attno_skip.  Just copy attribute's = input
+				 * key into arrayKeyData[] once outside this inner loop.
+				 *
+				 * Note: When we get here there must be a later attribute that
+				 * lacks an equality input key, and still needs a skip array
+				 * (if there wasn't then numSkipArrayKeys would be 0 by now).
+				 */
+				Assert(attno_skip == inkey->sk_attno);
+				/* inkey can't be last input key to be marked required: */
+				Assert(input_ikey < scan->numberOfKeys - 1);
+#if 0
+				/* Could be a redundant input scan key, so can't do this: */
+				Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+					   (inkey->sk_flags & SK_SEARCHNULL));
+#endif
+
+				attno_skip++;
+				break;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize generic BTArrayKeyInfo fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+
+			/* Initialize skip array specific BTArrayKeyInfo fields */
+			attr = TupleDescCompactAttr(RelationGetDescr(rel), attno_skip - 1);
+			reverse = (indoption[attno_skip - 1] & INDOPTION_DESC) != 0;
+			so->arrayKeys[numArrayKeys].attlen = attr->attlen;
+			so->arrayKeys[numArrayKeys].attbyval = attr->attbyval;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup =
+				PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse);
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * We'll need a 3-way ORDER proc to perform "binary searches" for
+			 * the next matching array element.  Set that up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			numArrayKeys++;
+			numArrayKeyData++;	/* keep this scan key/array */
+
+			/* set up next output scan key */
+			cur = &arrayKeyData[numArrayKeyData];
+
+			/* remember having output this skip array and scan key */
+			numSkipArrayKeys--;
+			attno_skip++;
+		}
 
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
-		*cur = scan->keyData[input_ikey];
+		*cur = *inkey;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -1257,7 +1527,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * all btree operators are strict.
 		 */
 		num_nonnulls = 0;
-		for (j = 0; j < num_elems; j++)
+		for (int j = 0; j < num_elems; j++)
 		{
 			if (!elem_nulls[j])
 				elem_values[num_nonnulls++] = elem_values[j];
@@ -1295,7 +1565,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -1306,7 +1576,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -1323,7 +1593,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -1392,23 +1662,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			origelemtype = elemtype;
 		}
 
-		/*
-		 * And set up the BTArrayKeyInfo data.
-		 *
-		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
-		 * scan_key field later on, after so->keyData[] has been finalized.
-		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		/* Initialize generic BTArrayKeyInfo fields */
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
+
+		/* Initialize SAOP array specific BTArrayKeyInfo fields */
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].cur_elem = -1;	/* i.e. invalid */
+
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
+	Assert(numSkipArrayKeys == 0);
+
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -1514,7 +1785,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -1565,7 +1837,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -1575,6 +1847,189 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit every so->arrayKeys[] entry.
+ *
+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller must add this many
+ * skip arrays to each of the most significant attributes lacking any keys
+ * that use the = strategy (IS NULL keys count as = keys here).  The specific
+ * attributes that need skip arrays are indicated by initializing caller's
+ * skip_eq_ops[] 0-based attribute offset to a valid = op strategy Oid.  We'll
+ * only ever set skip_eq_ops[] entries to InvalidOid for attributes that
+ * already have an equality key in scan->keyData[] input keys -- and only when
+ * there's some later "attribute gap" for us to "fill-in" with a skip array.
+ *
+ * We're optimistic about skipping working out: we always add exactly the skip
+ * arrays needed to maximize the number of input scan keys that can ultimately
+ * be marked as required to continue the scan (but no more).  For a composite
+ * index on (a, b, c, d), we'll instruct caller to add skip arrays as follows:
+ *
+ * Input keys						Output keys (after all preprocessing)
+ * ----------						-------------------------------------
+ * a = 1							a = 1 (no skip arrays)
+ * a = ANY('{0, 1}')				a = ANY('{0, 1}') (no skip arrays)
+ * b = 42							skip a AND b = 42
+ * b = ANY('{40, 42}')				skip a AND b = ANY('{40, 42}')
+ * a = 1 AND b = 42					a = 1 AND b = 42 (no skip arrays)
+ * a >= 1 AND b = 42				range skip a AND b = 42
+ * a = 1 AND b > 42					a = 1 AND b > 42 (no skip arrays)
+ * a >= 1 AND a <= 3 AND b = 42		range skip a AND b = 42
+ * a = 1 AND c <= 27				a = 1 AND skip b AND c <= 27
+ * a = 1 AND d >= 1					a = 1 AND skip b AND skip c AND d >= 1
+ * a = 1 AND b >= 42 AND d > 1		a = 1 AND range skip b AND skip c AND d > 1
+ */
+static int
+_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_skip = 1,
+				attno_inkey = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+#ifdef DEBUG_DISABLE_SKIP_SCAN
+	/* don't attempt to add skip arrays */
+	return numArrayKeys;
+#endif
+
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+			/* Look up input opclass's equality operator (might fail) */
+			skip_eq_ops[attno_skip - 1] =
+				get_opfamily_member(opfamily, opcintype, opcintype,
+									BTEqualStrategyNumber);
+			if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key
+		 * (doesn't matter whether that attribute has an equality key, or just
+		 * uses inequality keys).
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys
+			 */
+			if (attno_has_equal)
+			{
+				/* Attributes with an = key must have InvalidOid eq_op set */
+				skip_eq_ops[attno_skip - 1] = InvalidOid;
+			}
+			else
+			{
+				Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+				Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+				/* Look up input opclass's equality operator (might fail) */
+				skip_eq_ops[attno_skip - 1] =
+					get_opfamily_member(opfamily, opcintype, opcintype,
+										BTEqualStrategyNumber);
+
+				if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+				{
+					/*
+					 * Input opclass lacks an equality strategy operator, so
+					 * don't generate a skip array that definitely won't work
+					 */
+					break;
+				}
+
+				/* plan on adding a backfill skip array for this attribute */
+				(*numSkipArrayKeys)++;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required later on.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
 /*
  * _bt_find_extreme_element() -- get least or greatest array element
  *
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 0d0b93875..314911983 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -30,6 +30,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -71,7 +72,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -79,11 +80,24 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 *
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -412,6 +426,7 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 		memcpy(scan->keyData, scankey, scan->numberOfKeys * sizeof(ScanKeyData));
 	so->numberOfKeys = 0;		/* until _bt_preprocess_keys sets it */
 	so->numArrayKeys = 0;		/* ditto */
+	so->skipScan = false;		/* tracks if skip arrays are in use */
 }
 
 /*
@@ -538,10 +553,155 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume all input scankeys will be output with its own
+	 * SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * scan array (and associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		CompactAttribute *attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescCompactAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   array->attbyval, array->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array key, while freeing memory allocated for old
+		 * sk_argument where required
+		 */
+		if (!array->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -552,7 +712,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -575,16 +736,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -611,6 +772,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -655,7 +817,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -677,14 +839,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -722,7 +879,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -766,11 +923,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -809,7 +966,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -818,7 +975,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	}
 	/* Copy the authoritative shared primitive scan counter to local field */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -836,6 +993,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -845,7 +1003,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -855,14 +1013,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 12d3145b4..01821486b 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -964,6 +964,15 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  All
+	 * array keys are always considered = keys, but we'll sometimes need to
+	 * treat the current key value as if we were using an inequality strategy.
+	 * This happens whenever a skip array's scan key is found to have been set
+	 * to one of the special sentinel values representing an imaginary point
+	 * before/after some (or all) of the index's real indexable values.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1039,8 +1048,56 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is MINVAL, choose low_compare (when scanning
+				 * backwards it'll be MAXVAL, and we'll choose high_compare).
+				 *
+				 * Note: if the array's low_compare key makes 'chosen' NULL,
+				 * then we behave as if the array's first element is -inf,
+				 * except when !array->null_elem, which implies a usable NOT
+				 * NULL constraint (or an explicit IS NOT NULL input key).
+				 */
+				if (chosen != NULL &&
+					(chosen->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skipequalitykey = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					Assert(so->skipScan);
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MAXVAL));
+						chosen = array->low_compare;
+					}
+					else
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MINVAL));
+						chosen = array->high_compare;
+					}
+
+					Assert(chosen == NULL ||
+						   chosen->sk_attno == skipequalitykey->sk_attno);
+
+					if (!array->null_elem)
+						impliesNN = skipequalitykey;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1083,9 +1140,41 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 
 				/*
-				 * Done if that was the last attribute, or if next key is not
-				 * in sequence (implying no boundary key is available for the
-				 * next attribute).
+				 * If the key that we just added to startKeys[] is a skip
+				 * array = key whose current element is marked NEXT or PRIOR,
+				 * make strat_total > or < (and stop adding boundary keys).
+				 * This only happens when input opclass lacks skip support.
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(so->skipScan);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(strat_total == BTEqualStrategyNumber);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We'll never find an exact = match for a NEXT or PRIOR
+					 * sentinel sk_argument value, so there's no sense in
+					 * trying to save later boundary keys in startKeys[]
+					 */
+					break;
+				}
+
+				/*
+				 * Done if that was the last index attribute.  Also done if
+				 * there is a gap index attribute that lacks any usable keys
+				 * (only possible in edge cases where preprocessing could not
+				 * generate a skip array key that "fills the gap").
 				 */
 				if (i >= so->numberOfKeys ||
 					cur->sk_attno != curattr + 1)
@@ -1577,10 +1666,12 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * corresponding value from the last item on the page.  So checking with
 	 * the last item on the page would give a more precise answer.
 	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
+	 * We don't do this for the first page read by each (primitive) scan, to
+	 * avoid slowing down point queries.  They typically don't stand to gain
+	 * much when the optimization can be applied, and are more likely to
+	 * notice the overhead of the precheck.  Also avoid it during skip scans,
+	 * where individual primitive scans that don't just read one leaf page are
+	 * likely to advance their skip array(s) several times per leaf page read.
 	 *
 	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
 	 * just set a low-order required array's key to the best available match
@@ -1604,7 +1695,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !so->skipScan && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index f7e5cd02a..1c1ac1ec9 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -30,6 +30,17 @@
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									  int32 set_elem_result, Datum tupdatum, bool tupnull);
+static void _bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_array_set_low_or_high(Relation rel, ScanKey skey,
+									  BTArrayKeyInfo *array, bool low_not_high);
+static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -207,6 +218,7 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 	int32		result = 0;
 
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
+	Assert(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)));
 
 	if (tupnull)				/* NULL tupdatum */
 	{
@@ -283,6 +295,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -405,6 +419,164 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, since skip arrays don't really
+ * contain elements (they generate their array elements procedurally instead).
+ * Our interface matches that of _bt_binsrch_array_skey in every other way.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  We return -1 when tupdatum/tupnull is lower that any value
+ * within the range of the array, and 1 when it is higher than every value.
+ * Caller should pass *set_elem_result to _bt_skiparray_set_element to advance
+ * the array.
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (array->null_elem)
+	{
+		Assert(!array->low_compare && !array->high_compare);
+
+		*set_elem_result = 0;
+		return;
+	}
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+}
+
+/*
+ * _bt_skiparray_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Caller passes set_elem_result returned by _bt_binsrch_skiparray_skey for
+ * caller's tupdatum/tupnull.
+ *
+ * We copy tupdatum/tupnull into skey's sk_argument iff set_elem_result == 0.
+ * Otherwise, we set skey to either the lowest or highest value that's within
+ * the range of caller's skip array (whichever is the best available match to
+ * tupdatum/tupnull that is still within the range of the skip array according
+ * to _bt_binsrch_skiparray_skey/set_elem_result).
+ */
+static void
+_bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  int32 set_elem_result, Datum tupdatum, bool tupnull)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (set_elem_result)
+	{
+		/* tupdatum/tupnull is out of the range of the skip array */
+		Assert(!array->null_elem);
+
+		_bt_array_set_low_or_high(rel, skey, array, set_elem_result < 0);
+		return;
+	}
+
+	/* Advance skip array to tupdatum (or tupnull) value */
+	if (unlikely(tupnull))
+	{
+		_bt_skiparray_set_isnull(rel, skey, array);
+		return;
+	}
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen);
+}
+
+/*
+ * _bt_skiparray_set_isnull() -- set skip array scan key to NULL
+ */
+static void
+_bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->null_elem && !array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -414,29 +586,349 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_array_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_array_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  bool low_not_high)
+{
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting array to lowest element (according to low_compare) */
+		skey->sk_flags |= SK_BT_MINVAL;
+	}
+	else
+	{
+		/* Setting array to highest element (according to high_compare) */
+		skey->sk_flags |= SK_BT_MAXVAL;
+	}
+}
+
+/*
+ * _bt_array_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the minimum value within the range
+	 * of a skip array (often just -inf) is never decrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly decrement sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Decrement" from NULL to the high_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->high_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup->decrement(rel, skey->sk_argument, &uflow);
+	if (unlikely(uflow))
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_skiparray_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_array_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MINVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the maximum value within the range
+	 * of a skip array (often just +inf) is never incrementable
+	 */
+	if (skey->sk_flags & SK_BT_MAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly increment sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Increment" from NULL to the low_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->low_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup->increment(rel, skey->sk_argument, &oflow);
+	if (unlikely(oflow))
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_skiparray_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -452,6 +944,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -461,29 +954,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_array_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_array_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -543,6 +1037,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -550,7 +1045,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -562,16 +1056,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_array_set_low_or_high(rel, cur, array,
+								  ScanDirectionIsForward(dir));
 	}
 }
 
@@ -696,9 +1184,70 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))
+		{
+			/* Scankey has a valid/comparable sk_argument value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0)
+			{
+				/*
+				 * Interpret result in a way that takes NEXT/PRIOR into
+				 * account
+				 */
+				if (cur->sk_flags & SK_BT_NEXT)
+					result = -1;
+				else if (cur->sk_flags & SK_BT_PRIOR)
+					result = 1;
+
+				Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP));
+			}
+		}
+		else
+		{
+			/*
+			 * Current array element/array = scan key value is a sentinel
+			 * value that represents the lowest (or highest) possible value
+			 * that's still within the range of the array.
+			 *
+			 * We don't have a valid sk_argument value from = scan key.  Check
+			 * if tupdatum is within the range of skip array instead.
+			 */
+			BTArrayKeyInfo *array = NULL;
+
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			/*
+			 * Optimization: avoid uselessly evaluating array's high_compare
+			 * (or uselessly evaluating array's low_compare) by passing
+			 * cur_elem_trig=true, along with an inverted scan direction.
+			 */
+			_bt_binsrch_skiparray_skey(true, -dir, tupdatum, tupnull,
+									   array, cur, &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum satisfies both low_compare and high_compare, so
+				 * it's time to advance the array keys.
+				 *
+				 * Note: It's possible that the skip array will "advance" from
+				 * its MAXVAL (or MINVAL) representation to an alternative,
+				 * logically equivalent representation of the same value: a
+				 * representation where the = key gets a valid datum in its
+				 * sk_argument.  This is only possible when low_compare uses
+				 * the <= strategy (or high_compare uses the >= strategy).
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1030,18 +1579,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1066,18 +1606,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -1095,12 +1626,20 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			/*
 			 * Binary search for closest match that's available from the array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems != -1)
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
+			 */
+			else
+				_bt_binsrch_skiparray_skey(cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 		}
 		else
 		{
@@ -1176,11 +1715,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+		if (array)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->num_elems == -1)
+			{
+				/* Skip array "binary search" result determines new element */
+				_bt_skiparray_set_element(rel, cur, array, result,
+										  tupdatum, tupnull);
+			}
+			else if (array->cur_elem != set_elem)
+			{
+				/* SAOP array has new set_elem (returned by binary search) */
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
 		}
 	}
 
@@ -1610,10 +2159,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -1944,6 +2494,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key uses one of several sentinel values.  We just
+		 * fall back on _bt_tuple_before_array_skeys when we see such a value.
+		 */
+		if (key->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL |
+							 SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index dd6f5a15c..817ad358f 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -106,6 +106,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 2c325badf..303622f9d 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index f1e74f184..cd3ed44c8 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index e199f0716..1d3d901dc 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -371,6 +371,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index f279853de..4227ab1a7 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f23cfad71..244f48f4f 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index d3d1e485b..5c9d7c3d5 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -94,7 +94,6 @@
 
 #include "postgres.h"
 
-#include <ctype.h>
 #include <math.h>
 
 #include "access/brin.h"
@@ -193,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -214,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5759,6 +5762,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6817,6 +6906,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6826,17 +6962,21 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
+	double		correlation = 0;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
+	bool		set_correlation = false;
 	bool		eqQualHere;
-	bool		found_saop;
+	bool		found_array;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	bool		upper_inequal_col;
+	bool		lower_inequal_col;
 	double		num_sa_scans;
+	double		inequalselectivity;
 	ListCell   *lc;
 
 	/*
@@ -6852,16 +6992,21 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
-	found_saop = false;
+	found_array = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
+	upper_inequal_col = false;
+	lower_inequal_col = false;
 	num_sa_scans = 1;
+	inequalselectivity = 1;
 	foreach(lc, path->indexclauses)
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
@@ -6869,13 +7014,103 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		past_first_skipped = false;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
-			eqQualHere = false;
-			indexcol++;
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't come after a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT,
+							new_num_sa_scans;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						Assert(!set_correlation);
+						correlation = btcost_correlation(index, &vardata);
+						set_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (!past_first_skipped)
+				{
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is...
+					 */
+					if (!upper_inequal_col)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lower_inequal_col)
+						ndistinct += 1;
+
+					past_first_skipped = true;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				found_array = true;
+				new_num_sa_scans = num_sa_scans * ndistinct;
+
+				/*
+				 * Stop adding new skip arrays when the would-be new
+				 * num_sa_scans exceeds a third of the number of index pages
+				 */
+				if (ceil(index->pages * 0.3333333) < new_num_sa_scans)
+					break;
+
+				/* Done costing skipping for this index column */
+				num_sa_scans = new_num_sa_scans;
+				indexcol++;
+			}
+
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+				break;			/* no quals for indexcol (can't skip scan) */
+
+			/* reset for next indexcol */
+			eqQualHere = false;
+			upper_inequal_col = false;
+			lower_inequal_col = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6897,6 +7132,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6905,7 +7141,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				double		alength = estimate_array_length(root, other_operand);
 
 				clause_op = saop->opno;
-				found_saop = true;
+				found_array = true;
 				/* estimate SA descents by indexBoundQuals only */
 				if (alength > 1)
 					num_sa_scans *= alength;
@@ -6917,7 +7153,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skip scan purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6933,6 +7169,34 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct.
+					 *
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (!upper_inequal_col)
+							inequalselectivity =
+								Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+									DEFAULT_RANGE_INEQ_SEL);
+						upper_inequal_col = true;
+					}
+					else
+					{
+						if (!lower_inequal_col)
+							inequalselectivity =
+								Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+									DEFAULT_RANGE_INEQ_SEL);
+						lower_inequal_col = true;
+					}
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6948,7 +7212,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
-		!found_saop &&
+		!found_array &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
 	else
@@ -6988,14 +7252,18 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * of leaf pages (we make it 1/3 the total number of pages instead) to
 		 * give the btree code credit for its ability to continue on the leaf
 		 * level with low selectivity scans.
+		 *
+		 * Note: num_sa_scans is derived in a way that treats any skip scan
+		 * quals as if they were ScalarArrayOpExpr quals whose array has as
+		 * many distinct elements as possibly-matching values in the index.
 		 */
 		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
 		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
-		 * to integer.
+		 * ScalarArrayOpExpr quals (as well as any skip scan quals) included
+		 * in indexBoundQuals, and then round to integer.
 		 *
 		 * It is tempting to make genericcostestimate behave as if SAOP
 		 * clauses work in almost the same way as scalar operators during
@@ -7050,110 +7318,25 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * cost is somewhat arbitrarily set at 50x cpu_operator_cost per page
 	 * touched.  The number of such pages is btree tree height plus one (ie,
 	 * we charge for the leaf page too).  As above, charge once per estimated
-	 * SA index descent.
+	 * index descent.
 	 */
 	descentCost = (index->tree_height + 1) * DEFAULT_PAGE_CPU_MULTIPLIER * cpu_operator_cost;
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!set_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..2bd35d2d2
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns skip support struct, allocating in caller's memory
+ * context.  Otherwise returns NULL, indicating that operator class has no
+ * skip support function.
+ */
+SkipSupport
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse)
+{
+	Oid			skipSupportFunction;
+	SkipSupport sksup;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return NULL;
+
+	sksup = palloc(sizeof(SkipSupportData));
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return sksup;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index ba9bae050..f64fbf263 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2286,6 +2287,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 4f8402ef9..1b72059d2 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,6 +13,7 @@
 
 #include "postgres.h"
 
+#include <limits.h>
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
@@ -21,6 +22,7 @@
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -416,6 +418,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index d17fcbd5c..1f1aafb36 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -829,7 +829,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 053619624..7e23a7b6e 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 8879554c3..bfb1a286e 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -581,6 +581,47 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Index Only Scan using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Index Only Scan Backward using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 --
 -- Test for multilevel page deletion
 --
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index bd5f002cf..113fc3293 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2247,7 +2247,8 @@ SELECT count(*) FROM dupindexcols
 (1 row)
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 explain (costs off)
 SELECT unique1 FROM tenk1
@@ -2269,7 +2270,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2289,7 +2290,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2309,6 +2310,25 @@ ORDER BY thousand DESC, tenthous DESC;
         0 |     3000
 (2 rows)
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ Index Only Scan Backward using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > 995) AND (tenthous = ANY ('{998,999}'::integer[])))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+ thousand | tenthous 
+----------+----------
+      999 |      999
+      998 |      998
+(2 rows)
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -2339,6 +2359,45 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 ---------
 (0 rows)
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+                   QUERY PLAN                    
+-------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY (NULL::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY ('{NULL,NULL,NULL}'::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: ((unique1 IS NULL) AND (unique1 IS NULL))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+ unique1 
+---------
+(0 rows)
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
                                 QUERY PLAN                                 
@@ -2462,6 +2521,44 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 ---------
 (0 rows)
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > '-1'::integer) AND (thousand >= 0) AND (tenthous = 3000))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+(1 row)
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand < 3) AND (thousand <= 2) AND (tenthous = 1001))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        1 |     1001
+(1 row)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index f9db4032e..d86f178ad 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5325,9 +5325,10 @@ Function              | in_range(time without time zone,time without time zone,i
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql
index 670ad5c6e..68c61dbc7 100644
--- a/src/test/regress/sql/btree_index.sql
+++ b/src/test/regress/sql/btree_index.sql
@@ -327,6 +327,27 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 
 --
 -- Test for multilevel page deletion
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index be570da08..bb4782cae 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -852,7 +852,8 @@ SELECT count(*) FROM dupindexcols
   WHERE f1 BETWEEN 'WA' AND 'ZZZ' and id < 1000 and f1 ~<~ 'YX';
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 
 explain (costs off)
@@ -864,7 +865,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -874,7 +875,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -884,6 +885,15 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand DESC, tenthous DESC;
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -897,6 +907,21 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 
 SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('{33, 44}'::bigint[]);
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
 
@@ -942,6 +967,26 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
 SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b6c170ac2..f91b1d8ff 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -219,6 +219,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2685,6 +2686,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.47.2

v24-0002-Improve-nbtree-SAOP-primitive-scan-scheduling.patchapplication/x-patch; name=v24-0002-Improve-nbtree-SAOP-primitive-scan-scheduling.patchDownload
From 427e9e40bacbb3eb5b5c6c2e834395395802fe1d Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Fri, 14 Feb 2025 16:11:59 -0500
Subject: [PATCH v24 2/6] Improve nbtree SAOP primitive scan scheduling.

Add new primitive index scan scheduling heuristics that make
_bt_advance_array_keys avoid ending the ongoing primitive index scan
when it has already read more than one leaf page during the ongoing
primscan.  This tends to result in better decisions about how to start
and end primitive index scans with queries that have SAOP arrays with
many elements that are clustered together (e.g., contiguous integers).

Preparation for an upcoming patch that will add skip scan optimizations
to nbtree.  That'll work by adding skip arrays, which behave similarly
to SAOP arrays with many elements clustered together.

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wzkz0wPe6+02kr+hC+JJNKfGtjGTzpG3CFVTQmKwWNrXNw@mail.gmail.com
---
 src/include/access/nbtree.h           |   9 +-
 src/backend/access/nbtree/nbtsearch.c |  52 ++++----
 src/backend/access/nbtree/nbtutils.c  | 165 ++++++++++++++++----------
 3 files changed, 136 insertions(+), 90 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 000c7289b..599e6fa4c 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1043,8 +1043,8 @@ typedef struct BTScanOpaqueData
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
-	bool		scanBehind;		/* Last array advancement matched -inf attr? */
-	bool		oppositeDirCheck;	/* explicit scanBehind recheck needed? */
+	bool		scanBehind;		/* arrays might be ahead of scan's key space? */
+	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
 	BTArrayKeyInfo *arrayKeys;	/* info about each equality-type array key */
 	FmgrInfo   *orderProcs;		/* ORDER procs for required equality keys */
 	MemoryContext arrayContext; /* scan-lifespan context for array data */
@@ -1099,6 +1099,7 @@ typedef struct BTReadPageState
 	 * Input and output parameters, set and unset by both _bt_readpage and
 	 * _bt_checkkeys to manage precheck optimizations
 	 */
+	bool		firstpage;		/* on first page of current primitive scan? */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
 
@@ -1298,8 +1299,8 @@ extern int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir);
 extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 						  IndexTuple tuple, int tupnatts);
-extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
-								  IndexTuple finaltup);
+extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
+									 IndexTuple finaltup);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 941b4eaaf..12d3145b4 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1558,6 +1558,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.offnum = InvalidOffsetNumber;
 	pstate.skip = InvalidOffsetNumber;
 	pstate.continuescan = true; /* default assumption */
+	pstate.firstpage = firstPage;
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
 	pstate.rechecks = 0;
@@ -1603,7 +1604,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!firstPage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
@@ -1620,36 +1621,24 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	if (ScanDirectionIsForward(dir))
 	{
 		/* SK_SEARCHARRAY forward scans must provide high key up front */
-		if (arrayKeys && !P_RIGHTMOST(opaque))
+		if (arrayKeys)
 		{
-			ItemId		iid = PageGetItemId(page, P_HIKEY);
-
-			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
-
-			if (unlikely(so->oppositeDirCheck))
+			if (!P_RIGHTMOST(opaque))
 			{
-				Assert(so->scanBehind);
+				ItemId		iid = PageGetItemId(page, P_HIKEY);
 
-				/*
-				 * Last _bt_readpage call scheduled a recheck of finaltup for
-				 * required scan keys up to and including a > or >= scan key.
-				 *
-				 * _bt_checkkeys won't consider the scanBehind flag unless the
-				 * scan is stopped by a scan key required in the current scan
-				 * direction.  We need this recheck so that we'll notice when
-				 * all tuples on this page are still before the _bt_first-wise
-				 * start of matches for the current set of array keys.
-				 */
-				if (!_bt_oppodir_checkkeys(scan, dir, pstate.finaltup))
+				pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+				if (unlikely(so->scanBehind) &&
+					!_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup))
 				{
 					/* Schedule another primitive index scan after all */
 					so->currPos.moreRight = false;
 					so->needPrimScan = true;
 					return false;
 				}
-
-				/* Deliberately don't unset scanBehind flag just yet */
 			}
+			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
 		/* load items[] in ascending order */
@@ -1745,7 +1734,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 		 * only appear on non-pivot tuples on the right sibling page are
 		 * common.
 		 */
-		if (pstate.continuescan && !P_RIGHTMOST(opaque))
+		if (pstate.continuescan && !so->scanBehind && !P_RIGHTMOST(opaque))
 		{
 			ItemId		iid = PageGetItemId(page, P_HIKEY);
 			IndexTuple	itup = (IndexTuple) PageGetItem(page, iid);
@@ -1767,11 +1756,24 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	else
 	{
 		/* SK_SEARCHARRAY backward scans must provide final tuple up front */
-		if (arrayKeys && minoff <= maxoff && !P_LEFTMOST(opaque))
+		if (arrayKeys)
 		{
-			ItemId		iid = PageGetItemId(page, minoff);
+			if (minoff <= maxoff && !P_LEFTMOST(opaque))
+			{
+				ItemId		iid = PageGetItemId(page, minoff);
 
-			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+				pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+				if (unlikely(so->scanBehind) &&
+					!_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup))
+				{
+					/* Schedule another primitive index scan after all */
+					so->currPos.moreLeft = false;
+					so->needPrimScan = true;
+					return false;
+				}
+			}
+			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
 		/* load items[] in descending order */
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 693e43c67..f7e5cd02a 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -42,6 +42,8 @@ static bool _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 static bool _bt_verify_arrays_bt_first(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_verify_keys_with_arraykeys(IndexScanDesc scan);
 #endif
+static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
 							  bool advancenonrequired, bool prechecked, bool firstmatch,
@@ -874,11 +876,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 				all_required_satisfied = true,
 				all_satisfied = true;
 
-	/*
-	 * Unset so->scanBehind (and so->oppositeDirCheck) in case they're still
-	 * set from back when we dealt with the previous page's high key/finaltup
-	 */
-	so->scanBehind = so->oppositeDirCheck = false;
+	Assert(!so->scanBehind && !so->oppositeDirCheck);
 
 	if (sktrig_required)
 	{
@@ -1387,6 +1385,12 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * for next page's finaltup (and we skip it for this page's finaltup).
 		 */
 		so->oppositeDirCheck = true;	/* recheck next page's high key */
+
+		/*
+		 * Make sure that any non-required arrays are set to the first array
+		 * element for the current scan direction
+		 */
+		_bt_rewind_nonrequired_arrays(scan, dir);
 	}
 
 	/*
@@ -1429,14 +1433,12 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 (all_required_satisfied || oppodir_inequality_sktrig) &&
 			 unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup)))
 	{
-		/*
-		 * Make sure that any non-required arrays are set to the first array
-		 * element for the current scan direction
-		 */
 		_bt_rewind_nonrequired_arrays(scan, dir);
 		goto new_prim_scan;
 	}
 
+continue_scan:
+
 	/*
 	 * Stick with the ongoing primitive index scan for now.
 	 *
@@ -1458,8 +1460,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	if (so->scanBehind)
 	{
 		/* Optimization: skip by setting "look ahead" mechanism's offnum */
-		Assert(ScanDirectionIsForward(dir));
-		pstate->skip = pstate->maxoff + 1;
+		if (ScanDirectionIsForward(dir))
+			pstate->skip = pstate->maxoff + 1;
+		else
+			pstate->skip = pstate->minoff - 1;
 	}
 
 	/* Caller's tuple doesn't match the new qual */
@@ -1469,6 +1473,37 @@ new_prim_scan:
 
 	Assert(pstate->finaltup);	/* not on rightmost/leftmost page */
 
+	/*
+	 * Looks like another primitive index scan is required.  But consider
+	 * backing out and continuing the primscan based on scan-level heuristics.
+	 *
+	 * Continue the ongoing primitive scan (but schedule a recheck for when
+	 * the scan arrives on the next sibling leaf page) when it has already
+	 * read at least one leaf page before the one we're reading now.  This is
+	 * important when reading subsets of an index with many distinct values in
+	 * respect of an attribute constrained by an array.  It encourages fewer,
+	 * larger primitive scans where that makes sense.
+	 *
+	 * Note: This heuristic isn't as aggressive as you might think.  We're
+	 * conservative about allowing a primitive scan to step from the first
+	 * leaf page it reads to the page's sibling page (we only allow it on
+	 * first pages whose finaltup strongly suggests that it'll work out).
+	 * Clearing this first page finaltup hurdle is a strong signal in itself.
+	 */
+	if (!pstate->firstpage)
+	{
+		/* Schedule a recheck once on the next (or previous) page */
+		so->scanBehind = true;
+		if (has_required_opposite_direction_only)
+			so->oppositeDirCheck = true;
+
+		/* Defensively reset any nonrequired SAOP arrays */
+		_bt_rewind_nonrequired_arrays(scan, dir);
+
+		/* Continue the current primitive scan after all */
+		goto continue_scan;
+	}
+
 	/*
 	 * End this primitive index scan, but schedule another.
 	 *
@@ -1634,6 +1669,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
+	Assert(!so->scanBehind && !so->oppositeDirCheck);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
 							arrayKeys, pstate->prechecked, pstate->firstmatch,
@@ -1688,62 +1724,36 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
+		/* Override _bt_check_compare, continue primitive scan */
+		pstate->continuescan = true;
+
 		/*
-		 * Tuple is still before the start of matches according to the scan's
-		 * required array keys (according to _all_ of its required equality
-		 * strategy keys, actually).
+		 * We will end up here repeatedly given a group of tuples > the
+		 * previous array keys and < the now-current keys (for a backwards
+		 * scan it's just the same, though the operators swap positions).
 		 *
-		 * _bt_advance_array_keys occasionally sets so->scanBehind to signal
-		 * that the scan's current position/tuples might be significantly
-		 * behind (multiple pages behind) its current array keys.  When this
-		 * happens, we need to be prepared to recover by starting a new
-		 * primitive index scan here, on our own.
+		 * We must avoid allowing this linear search process to scan very many
+		 * tuples from well before the start of tuples matching the current
+		 * array keys (or from well before the point where we'll once again
+		 * have to advance the scan's array keys).
+		 *
+		 * We keep the overhead under control by speculatively "looking ahead"
+		 * to later still-unscanned items from this same leaf page. We'll only
+		 * attempt this once the number of tuples that the linear search
+		 * process has examined starts to get out of hand.
 		 */
-		Assert(!so->scanBehind ||
-			   so->keyData[ikey].sk_strategy == BTEqualStrategyNumber);
-		if (unlikely(so->scanBehind) && pstate->finaltup &&
-			_bt_tuple_before_array_skeys(scan, dir, pstate->finaltup, tupdesc,
-										 BTreeTupleGetNAtts(pstate->finaltup,
-															scan->indexRelation),
-										 false, 0, NULL))
+		pstate->rechecks++;
+		if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
 		{
-			/* Cut our losses -- start a new primitive index scan now */
-			pstate->continuescan = false;
-			so->needPrimScan = true;
-		}
-		else
-		{
-			/* Override _bt_check_compare, continue primitive scan */
-			pstate->continuescan = true;
+			/* See if we should skip ahead within the current leaf page */
+			_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
 
 			/*
-			 * We will end up here repeatedly given a group of tuples > the
-			 * previous array keys and < the now-current keys (for a backwards
-			 * scan it's just the same, though the operators swap positions).
-			 *
-			 * We must avoid allowing this linear search process to scan very
-			 * many tuples from well before the start of tuples matching the
-			 * current array keys (or from well before the point where we'll
-			 * once again have to advance the scan's array keys).
-			 *
-			 * We keep the overhead under control by speculatively "looking
-			 * ahead" to later still-unscanned items from this same leaf page.
-			 * We'll only attempt this once the number of tuples that the
-			 * linear search process has examined starts to get out of hand.
+			 * Might have set pstate.skip to a later page offset.  When that
+			 * happens then _bt_readpage caller will inexpensively skip ahead
+			 * to a later tuple from the same page (the one just after the
+			 * tuple we successfully "looked ahead" to).
 			 */
-			pstate->rechecks++;
-			if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
-			{
-				/* See if we should skip ahead within the current leaf page */
-				_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
-
-				/*
-				 * Might have set pstate.skip to a later page offset.  When
-				 * that happens then _bt_readpage caller will inexpensively
-				 * skip ahead to a later tuple from the same page (the one
-				 * just after the tuple we successfully "looked ahead" to).
-				 */
-			}
 		}
 
 		/* This indextuple doesn't match the current qual, in any case */
@@ -1760,6 +1770,39 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 								  ikey, true);
 }
 
+/*
+ * Test whether finaltup (the final tuple on the page) is still before the
+ * start of matches for the current array keys.
+ *
+ * Caller's finaltup tuple is the page high key (for forwards scans), or the
+ * first non-pivot tuple (for backwards scans).  Called during scans with
+ * array keys when the so->scanBehind flag was set on the previous page.
+ *
+ * Returns false if the tuple is still before the start of matches.  When that
+ * happens caller should cut its losses and start a new primitive index scan.
+ * Otherwise returns true.
+ */
+bool
+_bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
+						 IndexTuple finaltup)
+{
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	int			nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+
+	Assert(so->numArrayKeys);
+
+	if (_bt_tuple_before_array_skeys(scan, dir, finaltup, tupdesc,
+									 nfinaltupatts, false, 0, NULL))
+		return false;
+
+	if (!so->oppositeDirCheck)
+		return true;
+
+	return _bt_oppodir_checkkeys(scan, dir, finaltup);
+}
+
 /*
  * Test whether an indextuple fails to satisfy an inequality required in the
  * opposite direction only.
@@ -1778,7 +1821,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
  * _bt_checkkeys to stop the scan to consider array advancement/starting a new
  * primitive index scan.
  */
-bool
+static bool
 _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 					  IndexTuple finaltup)
 {
-- 
2.47.2

v24-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/x-patch; name=v24-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From 5238ed4044ba5df7a2c0313c4d85df76b0cfa268 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v24 1/6] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.

This information is made more important still by an upcoming patch that
adds skip scan optimizations to nbtree.  The patch implements skip scan
by generating "skip arrays" during nbtree preprocessing, which makes the
relationship between the total number of primitive index scans and the
scan qual looser still.  The new instrumentation will help users to
understand how effective these skip scan optimizations are in practice.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  11 ++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  46 ++++++++
 contrib/bloom/blscan.c                        |   1 +
 doc/src/sgml/bloom.sgml                       |   7 +-
 doc/src/sgml/monitoring.sgml                  |  12 ++-
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 22 files changed, 244 insertions(+), 46 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index dc6e01842..0d1ad8379 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -147,6 +147,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 4265687af..f75abf867 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -587,6 +587,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index 7d1e66152..81440a283 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index cc40e928e..609e85fda 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index a3a1fccf3..c4f730437 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 07bae342e..369e39bc4 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index dc244ae24..0d0b93875 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -70,6 +70,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -555,6 +556,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -581,6 +583,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -708,6 +711,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			Assert(btscan->btps_nextScanPage != P_NONE);
 			*next_scan_page = btscan->btps_nextScanPage;
@@ -808,6 +816,8 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+	/* Copy the authoritative shared primitive scan counter to local field */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -842,6 +852,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 472ce06f1..941b4eaaf 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -950,6 +950,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 53f910e9d..8554b4535 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c24e66f82..6b65037cd 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2105,6 +2107,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2118,6 +2121,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2134,6 +2138,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2652,6 +2657,47 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+	double		nloops;
+
+	if (!es->analyze)
+		return;
+
+	nloops = planstate->instrument->nloops;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	if (nloops > 0)
+		ExplainPropertyFloat("Index Searches", NULL, nsearches / nloops, 0, es);
+	else
+		ExplainPropertyFloat("Index Searches", NULL, 0.0, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index bf801fe78..472169d61 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -116,6 +116,7 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++)
 	{
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 6a8a60b8c..4cddfaae1 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -174,9 +174,10 @@ CREATE INDEX
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..178436.00 rows=1 width=0) (actual time=20.005..20.005 rows=2300 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
          Buffers: shared hit=19608
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 22.632 ms
-(10 rows)
+(11 rows)
 </programlisting>
   </para>
 
@@ -209,12 +210,14 @@ CREATE INDEX
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..4.52 rows=11 width=0) (actual time=0.026..0.026 rows=7 loops=1)
                Index Cond: (i5 = 123451)
                Buffers: shared hit=3
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..4.52 rows=11 width=0) (actual time=0.007..0.007 rows=8 loops=1)
                Index Cond: (i2 = 898732)
                Buffers: shared hit=3
+               Index Searches: 1
  Planning Time: 0.264 ms
  Execution Time: 0.047 ms
-(13 rows)
+(15 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 928a6eb64..e7aeeb730 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4262,12 +4262,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage
     Queries that use certain <acronym>SQL</acronym> constructs to search for
     rows matching any value out of a list or array of multiple scalar values
     (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    index searches (up to one index search per scalar value) during query
+    execution.  Each internal index search increments
+    <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    <command>EXPLAIN ANALYZE</command> breaks down the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
   </note>
 
  </sect2>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aab..34225c010 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -730,9 +730,11 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
                Buffers: shared hit=2
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
          Buffers: shared hit=24 read=6
+         Index Searches: 1
  Planning:
    Buffers: shared hit=15 dirtied=9
  Planning Time: 0.485 ms
@@ -791,6 +793,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
                            Buffers: shared hit=2
+                           Index Searches: 1
  Planning:
    Buffers: shared hit=12
  Planning Time: 0.187 ms
@@ -860,6 +863,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
    Buffers: shared hit=1
  Planning Time: 0.039 ms
@@ -894,8 +898,10 @@ EXPLAIN (ANALYZE, BUFFERS OFF) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND un
    -&gt;  BitmapAnd  (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
  Planning Time: 0.162 ms
  Execution Time: 0.143 ms
 </screen>
@@ -924,6 +930,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
                Buffers: shared read=2
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1059,6 +1066,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
    Buffers: shared hit=16
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
          Buffers: shared hit=16
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index 6361a14e6..6fc9bfedc 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -506,9 +506,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
          Buffers: shared hit=4
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(9 rows)
+(10 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 9fdf8b1d9..80a63b5f1 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index f2d146581..a5df4107a 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 5ecf971da..d9df5c0e1 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index e667503c9..6bc6da334 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2369,6 +2369,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2686,47 +2690,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2742,16 +2755,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2770,7 +2786,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2786,16 +2802,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2816,7 +2835,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2887,16 +2906,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2904,17 +2926,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2990,17 +3015,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -3011,17 +3042,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3056,17 +3093,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=5 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3077,17 +3120,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3141,17 +3190,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3173,17 +3228,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3511,12 +3572,14 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3532,9 +3595,10 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3582,13 +3646,17 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4158,14 +4226,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 88911ca2b..cde2eac73 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index d5aab4e56..2197b0ac5 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 730545e86..7e7c75699 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -588,6 +588,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.47.2

In reply to: Peter Geoghegan (#64)
6 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Feb 14, 2025 at 6:06 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v24, which breaks out these recent changes to primscan
scheduling into their own commit/patch (namely
v24-0002-Improve-nbtree-SAOP-primitive-scan-scheduling.patch). The
primscan scheduling improvement stuff hasn't really changed since
v23, though (though I did polish it some more). I hope to be able to
commit this new primscan scheduling patch sooner rather than later
(though I don't think that it's quite committable yet).

The patch series recently bitrot due to conflicting changes on HEAD,
so I decided to post a new v25 now.

New in v25:

* Fixed a regression with parallel index scans caused by the improved
scheduling logic added by
0002-Improve-nbtree-SAOP-primitive-scan-scheduling.patch.

v24 did not account for how "firstPage=false" calls to _bt_readpage
might originate from _bt_first (not _bt_next) during parallel scans,
even during the first page for the parallel worker's primitive
scan/_bt_first call -- which made the heuristics added to
_bt_advance_array_keys do the wrong thing by not terminating primitive
scan based on faulty information. This was possible via the parallel
scan _bt_readnextpage "seized=true" path, which led to regressions. In
v24 we now pass "firstPage=true" whenever a call to _bt_readpage
happens through _bt_first, no matter the details (for the first
_bt_readpage call's page).

* The additional EXPLAIN ANALYZE logic (that shows "Index Searches:
N") added by 0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patch has
been adjusted, and no longer divides by nloops.

I explain why dividing by nloops has some fairly bad consequences on
the thread I started for the quasi-independent enhancement to EXPLAIN
ANALYZE output:

/messages/by-id/CAH2-WzmebSkeKPGw7TEaNw9=Qx-X8fAnFw916Fd2V8VVqYqqaQ@mail.gmail.com

* Polished commit messages.

* Added more test coverage. The LOC covered percentage is now at just
over 90% for nbtutils.c. We now have coverage for almost all of the
new code that advances the scan's skip arrays, including code that
deals with NULL values that is seldom reached.

--
Peter Geoghegan

Attachments:

v25-0004-Lower-the-overhead-of-nbtree-runtime-skip-checks.patchapplication/x-patch; name=v25-0004-Lower-the-overhead-of-nbtree-runtime-skip-checks.patchDownload
From a6e451087883ef894ca1869be532f0159ee26a1e Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v25 4/6] Lower the overhead of nbtree runtime skip checks.

Add a new optimization that fixes regressions in index scans that are
nominally eligible to use skip scan, but can never actually benefit from
skipping.  These are cases where a leading prefix column contains many
distinct values -- especially when the number of values approaches the
total number of index tuples, where skipping cannot possibly help.

The optimization is activated dynamically, as our fall back strategy.
It works by determining a prefix of leading index columns whose scan
keys (often skip array scan keys) are guaranteed to be satisfied by
every possible index tuple from a given page.  _bt_readpage is then able
to start comparisons at the first scan key that might not be satisfied.
This necessitates making _bt_readpage temporarily cease maintaining the
scan's arrays during _bt_checkkeys calls prior to the finaltup call
(_bt_checkkeys treats the scan's keys as nonrequired, including both
SAOP arrays and skip arrays).

This optimization has no impact on primitive index scan scheduling.  It
is similar to the precheck optimizations added by commit e0b1ee17dc,
though it is only used during nbtree index scans that use skip arrays.
The optimization can even improve performance with certain queries that
use inequality range skip arrays -- even when there is no benefit from
skipping.  This is possible wherever the new optimization lowers the
absolute number of scan key comparisons.

Skip scan's approach of adding skip arrays during preprocessing, and
fixing (or significantly ameliorating) the resulting regressions in
unsympathetic cases is enabled by the optimization added by this commit
(and by the "look ahead" array optimization added by commit 5bf748b8).
This enables skip scan's approach within the planner: we don't create
distinct index paths to represent nbtree index skip scans.  Whether and
to what extent a scan actually skips is always determined dynamically,
at runtime.  This makes affected nbtree index scans robust against data
skew, cardinality estimation errors, and cost estimation errors.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h           |   5 +-
 src/backend/access/nbtree/nbtsearch.c |  48 ++++
 src/backend/access/nbtree/nbtutils.c  | 383 +++++++++++++++++++++++---
 3 files changed, 395 insertions(+), 41 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 10d9f8186..ef20e9932 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1115,11 +1115,13 @@ typedef struct BTReadPageState
 
 	/*
 	 * Input and output parameters, set and unset by both _bt_readpage and
-	 * _bt_checkkeys to manage precheck optimizations
+	 * _bt_checkkeys to manage precheck and forcenonrequired optimizations
 	 */
 	bool		firstpage;		/* on first page of current primitive scan? */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
+	bool		forcenonrequired;	/* treat all scan keys as nonrequired? */
+	int			ikey;			/* start comparisons from ikey'th scan key */
 
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
@@ -1328,6 +1330,7 @@ extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arra
 						  IndexTuple tuple, int tupnatts);
 extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
 									 IndexTuple finaltup);
+extern void _bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 3e06a260e..45ed6e489 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1650,6 +1650,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.firstpage = firstpage;
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
+	pstate.forcenonrequired = false;
+	pstate.ikey = 0;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
@@ -1732,6 +1734,14 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 													   so->currPos.currPage);
 					return false;
 				}
+
+				/*
+				 * Consider temporarily disabling array maintenance during
+				 * skip scan primitive index scans that have read more than
+				 * one leaf page (unless we've reached the rightmost page)
+				 */
+				if (!pstate.firstpage && so->skipScan && minoff < maxoff)
+					_bt_skip_ikeyprefix(scan, &pstate);
 			}
 
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
@@ -1773,6 +1783,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum < pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1836,6 +1847,15 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			IndexTuple	itup = (IndexTuple) PageGetItem(page, iid);
 			int			truncatt;
 
+			if (pstate.forcenonrequired)
+			{
+				Assert(so->skipScan);
+
+				/* recover from treating the scan's keys as nonrequired */
+				_bt_start_array_keys(scan, dir);
+				pstate.forcenonrequired = false;
+				pstate.ikey = 0;
+			}
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
@@ -1872,6 +1892,14 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 													   so->currPos.currPage);
 					return false;
 				}
+
+				/*
+				 * Consider temporarily disabling array maintenance during
+				 * skip scan primitive index scans that have read more than
+				 * one leaf page (unless we've reached the leftmost page)
+				 */
+				if (!pstate.firstpage && so->skipScan && minoff < maxoff)
+					_bt_skip_ikeyprefix(scan, &pstate);
 			}
 
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
@@ -1916,6 +1944,15 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff && pstate.forcenonrequired)
+			{
+				Assert(so->skipScan);
+
+				/* recover from treating the scan's keys as nonrequired */
+				_bt_start_array_keys(scan, dir);
+				pstate.forcenonrequired = false;
+				pstate.ikey = 0;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
@@ -1927,6 +1964,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum > pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1992,6 +2030,16 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 		so->currPos.itemIndex = MaxTIDsPerBTreePage - 1;
 	}
 
+	/*
+	 * As far as our caller is concerned, the scan's arrays always track its
+	 * progress through the index's key space.
+	 *
+	 * If _bt_skip_ikeyprefix told us to temporarily treat all scan keys as
+	 * nonrequired (during a skip scan), then we must recover afterwards by
+	 * advancing our arrays using finaltup (with !pstate.forcenonrequired).
+	 */
+	Assert(!pstate.forcenonrequired);
+
 	return (so->currPos.firstItem <= so->currPos.lastItem);
 }
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index b45e3d32a..9e76971ca 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -57,11 +57,12 @@ static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool forcenonrequired,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-								 ScanDirection dir, bool *continuescan);
+								 ScanDirection dir, bool forcenonrequired, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -1415,14 +1416,14 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
  * postcondition's <= operator with a >=.  In other words, just swap the
  * precondition with the postcondition.)
  *
- * We also deal with "advancing" non-required arrays here.  Callers whose
- * sktrig scan key is non-required specify sktrig_required=false.  These calls
- * are the only exception to the general rule about always advancing the
+ * We also deal with "advancing" non-required arrays here (or arrays that are
+ * treated as non-required for the duration of a _bt_readpage call).  Callers
+ * whose sktrig scan key is non-required specify sktrig_required=false.  These
+ * calls are the only exception to the general rule about always advancing the
  * required array keys (the scan may not even have a required array).  These
  * callers should just pass a NULL pstate (since there is never any question
  * of stopping the scan).  No call to _bt_tuple_before_array_skeys is required
- * ahead of these calls (it's already clear that any required scan keys must
- * be satisfied by caller's tuple).
+ * ahead of these calls.
  *
  * Note that we deal with non-array required equality strategy scan keys as
  * degenerate single element arrays here.  Obviously, they can never really
@@ -1474,8 +1475,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		pstate->firstmatch = false;
 
-		/* Shouldn't have to invalidate 'prechecked', though */
-		Assert(!pstate->prechecked);
+		/* Shouldn't have to invalidate precheck/forcenonrequired state */
+		Assert(!pstate->prechecked && !pstate->forcenonrequired &&
+			   pstate->ikey == 0);
 
 		/*
 		 * Once we return we'll have a new set of required array keys, so
@@ -1484,6 +1486,26 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		pstate->rechecks = 0;
 		pstate->targetdistance = 0;
 	}
+	else if (sktrig < so->numberOfKeys - 1 &&
+			 !(so->keyData[so->numberOfKeys - 1].sk_flags & SK_SEARCHARRAY))
+	{
+		int			least_sign_ikey = so->numberOfKeys - 1;
+		bool		continuescan;
+
+		/*
+		 * Optimization: perform a precheck of the least significant key
+		 * during !sktrig_required calls when it isn't already our sktrig
+		 * (provided the precheck key is not itself an array).
+		 *
+		 * When the precheck works out we'll avoid an expensive binary search
+		 * of sktrig's array (plus any other arrays before least_sign_ikey).
+		 */
+		Assert(so->keyData[sktrig].sk_flags & SK_SEARCHARRAY);
+		if (!_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
+							   false, false, false, false,
+							   &continuescan, &least_sign_ikey))
+			return false;
+	}
 
 	Assert(_bt_verify_keys_with_arraykeys(scan));
 
@@ -1527,8 +1549,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -1662,7 +1682,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -1704,7 +1724,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -1719,7 +1739,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -1779,6 +1799,12 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * of any required scan key).  All that matters is whether caller's tuple
 	 * satisfies the new qual, so it's safe to just skip the _bt_check_compare
 	 * recheck when we've already determined that it can only return 'false'.
+	 *
+	 * Note: In practice most scan keys are marked required by preprocessing,
+	 * if necessary by generating a preceding skip array.  We nevertheless
+	 * often handle array keys marked required as if they were nonrequired.
+	 * This behavior is requested by our _bt_check_compare caller, though only
+	 * when it is passed "forcenonrequired=true" by _bt_checkkeys.
 	 */
 	if ((sktrig_required && all_required_satisfied) ||
 		(!sktrig_required && all_satisfied))
@@ -1790,7 +1816,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -2034,8 +2060,10 @@ new_prim_scan:
 	 * the scan arrives on the next sibling leaf page) when it has already
 	 * read at least one leaf page before the one we're reading now.  This is
 	 * important when reading subsets of an index with many distinct values in
-	 * respect of an attribute constrained by an array.  It encourages fewer,
-	 * larger primitive scans where that makes sense.
+	 * respect of an attribute constrained by an array (often a skip array).
+	 * It encourages fewer, larger primitive scans where that makes sense.
+	 * This will in turn encourage _bt_readpage to apply the forcenonrequired
+	 * optimization when applicable (i.e. when the scan has a skip array).
 	 *
 	 * Note: This heuristic isn't as aggressive as you might think.  We're
 	 * conservative about allowing a primitive scan to step from the first
@@ -2217,14 +2245,15 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	TupleDesc	tupdesc = RelationGetDescr(scan->indexRelation);
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ScanDirection dir = so->currPos.dir;
-	int			ikey = 0;
+	int			ikey = pstate->ikey;
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+							arrayKeys, pstate->forcenonrequired,
+							pstate->prechecked, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
@@ -2235,22 +2264,23 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 *
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
-		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
+		Assert(!pstate->prechecked && !pstate->firstmatch &&
+			   !pstate->forcenonrequired);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
 	if (pstate->prechecked || pstate->firstmatch)
 	{
 		bool		dcontinuescan;
-		int			dikey = 0;
+		int			dikey = pstate->ikey;
 
 		/*
 		 * Call relied on continuescan/firstmatch prechecks -- assert that we
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, pstate->forcenonrequired,
+										false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -2273,6 +2303,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * It's also possible that the scan is still _before_ the _start_ of
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
+	Assert(!pstate->forcenonrequired);
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
@@ -2388,7 +2419,7 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	Assert(so->numArrayKeys);
 
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -2396,6 +2427,232 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	return true;
 }
 
+/*
+ * Determine a prefix of scan keys that are guaranteed to be satisfied by
+ * every possible tuple on pstate's page.  Used during scans with skip arrays,
+ * which do not use the similar optimization controlled by pstate.prechecked.
+ *
+ * Sets pstate.ikey (and pstate.forcenonrequired) on success, making later
+ * calls to _bt_checkkeys start checks of each tuple from the so->keyData[]
+ * entry at pstate.ikey (while treating keys >= pstate.ikey as nonrequired).
+ *
+ * When _bt_checkkeys treats the scan's required keys as non-required, the
+ * scan's array keys won't be properly maintained (they won't have advanced in
+ * lockstep with our progress through the index's key space as expected).
+ * Caller must recover from this by restarting the scan's array keys and
+ * resetting pstate.ikey and pstate.forcenonrequired just ahead of the
+ * _bt_checkkeys call for the page's final tuple (the pstate.finaltup tuple).
+ */
+void
+_bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	ScanDirection dir = so->currPos.dir;
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	ItemId		iid;
+	IndexTuple	firsttup,
+				lasttup;
+	int			ikey = 0,
+				arrayidx = 0,
+				firstchangingattnum;
+
+	Assert(so->skipScan && pstate->minoff < pstate->maxoff);
+
+	/* minoff is an offset to the lowest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->minoff);
+	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* maxoff is an offset to the highest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->maxoff);
+	lasttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* Determine the first attribute whose values change on caller's page */
+	firstchangingattnum = _bt_keep_natts_fast(rel, firsttup, lasttup);
+
+	for (; ikey < so->numberOfKeys; ikey++)
+	{
+		ScanKey		key = so->keyData + ikey;
+		BTArrayKeyInfo *array;
+		Datum		tupdatum;
+		bool		tupnull;
+		int32		result;
+
+		/*
+		 * Determine if it's safe to set pstate.ikey to an offset to a key
+		 * that comes after this key, by examining this key
+		 */
+		if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) == 0)
+		{
+			/*
+			 * This is the first key that is not marked required (only happens
+			 * in corner cases where _bt_preprocess_keys couldn't mark all
+			 * keys required due to restrictions on generating skip arrays)
+			 */
+			Assert(!(key->sk_flags & SK_BT_SKIP));
+			break;				/* pstate.ikey to be set to nonrequired ikey */
+		}
+		if (key->sk_strategy != BTEqualStrategyNumber)
+		{
+			/*
+			 * This is the scan's first inequality key (barring inequalities
+			 * that are used to describe the range of a = skip array key).
+			 *
+			 * We could handle this like a = key, but it doesn't seem worth
+			 * the trouble.  Have _bt_checkkeys start with this inequality.
+			 */
+			break;				/* pstate.ikey to be set to inequality's ikey */
+		}
+		if (!(key->sk_flags & SK_SEARCHARRAY))
+		{
+			/*
+			 * Found a scalar (non-array) = key.
+			 *
+			 * It is unsafe to set pstate.ikey to an ikey beyond this key,
+			 * unless the = key is satisfied by every possible tuple on the
+			 * page (possible only when attribute has just one distinct value
+			 * among all tuples on the page).
+			 */
+			if (key->sk_attno < firstchangingattnum)
+			{
+				/*
+				 * Only one distinct value on the page for this key's column.
+				 * Must make sure that = key is actually satisfied by the
+				 * value that is stored within every tuple on the page.
+				 */
+				tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+										 &tupnull);
+				result = _bt_compare_array_skey(&so->orderProcs[ikey],
+												tupdatum, tupnull,
+												key->sk_argument, key);
+				if (result == 0)
+					continue;	/* safe, = key satisfied by every tuple */
+			}
+			break;				/* pstate.ikey to be set to scalar key's ikey */
+		}
+
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == ikey);
+		if (array->num_elems != -1)
+		{
+			/*
+			 * Found a SAOP array = key.
+			 *
+			 * Handle this just like we handle scalar = keys.
+			 */
+			if (key->sk_attno < firstchangingattnum)
+			{
+				/*
+				 * Only one distinct value on the page for this key's column.
+				 * Must make sure that SAOP array is actually satisfied by the
+				 * value that is stored within every tuple on the page.
+				 */
+				tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+										 &tupnull);
+				_bt_binsrch_array_skey(&so->orderProcs[ikey], false,
+									   NoMovementScanDirection,
+									   tupdatum, tupnull, array, key, &result);
+				if (result == 0)
+					continue;	/* safe, SAOP = key satisfied by every tuple */
+			}
+			break;				/* pstate.ikey to be set to SAOP array's ikey */
+		}
+
+		/*
+		 * Found a skip array = key.
+		 *
+		 * As with other = keys, moving past this skip array key is safe when
+		 * every tuple on the page is guaranteed to satisfy the array's key.
+		 * But we need a slightly different approach, since skip arrays make
+		 * it easy to assess whether all the values on the page fall within
+		 * the skip array's entire range.
+		 */
+		if (array->null_elem)
+		{
+			/* Safe, non-range skip array "satisfied" by every tuple on page */
+			continue;
+		}
+		else if (key->sk_attno > firstchangingattnum)
+		{
+			/*
+			 * We cannot assess whether this range skip array will definitely
+			 * be satisfied by every tuple on the page, since its attribute is
+			 * preceded by another attribute that does not contain the same
+			 * prefix of value(s) within every tuple from caller's page
+			 */
+			break;				/* pstate.ikey to be set to range array's ikey */
+		}
+
+		/*
+		 * Found a range skip array = key.
+		 *
+		 * It's definitely safe for _bt_checkkeys to avoid assessing this
+		 * range skip array when the page's first and last non-pivot tuples
+		 * both satisfy the range skip array (since the same must also be true
+		 * of all the tuples in between these two).
+		 */
+		tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   tupdatum, tupnull, array, key, &result);
+		if (result != 0)
+			break;				/* pstate.ikey to be set to range array's ikey */
+
+		tupdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   tupdatum, tupnull, array, key, &result);
+		if (result != 0)
+			break;				/* pstate.ikey to be set to range array's ikey */
+
+		/* Safe, range skip array satisfied by every tuple on page */
+	}
+
+	/*
+	 * If pstate.ikey remains 0, _bt_advance_array_keys will still be able to
+	 * apply its precheck optimization when dealing with "nonrequired" array
+	 * keys.  That's reason enough on its own to set forcenonrequired=true
+	 * (it'll usually also make it safe to use a non-zero pstate.ikey).
+	 */
+	pstate->forcenonrequired = true;	/* do this unconditionally */
+	pstate->ikey = ikey;
+
+	/*
+	 * Set the element for range skip arrays whose ikey is >= pstate.ikey to
+	 * whatever the first array element is in the scan's current direction.
+	 * This allows range skip arrays that will never be satisfied by any tuple
+	 * on the page to avoid extra sk_argument comparisons -- _bt_check_compare
+	 * won't use the key's sk_argument when the key is marked MINVAL/MAXVAL
+	 * (note that MINVAL/MAXVAL won't be unset until an exact match is found,
+	 * which might not happen for any tuple on the page).
+	 *
+	 * Set the element for non-range skip arrays whose ikey is >= pstate.ikey
+	 * to NULL (regardless of whether NULLs are stored first or last), too.
+	 * This allows non-range skip arrays (recognized by _bt_check_compare as
+	 * "non-required" skip arrays with ISNULL set) to avoid needlessly calling
+	 * _bt_advance_array_keys (we know that any non-range skip array must be
+	 * satisfied by every possible indexable value, so this is always safe).
+	 */
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		key = &so->keyData[array->scan_key];
+
+		Assert(key->sk_flags & SK_SEARCHARRAY);
+
+		if (array->num_elems != -1)
+			continue;
+		if (array->scan_key < pstate->ikey)
+			continue;
+
+		Assert(key->sk_flags & SK_BT_SKIP);
+
+		if (!array->null_elem)
+			_bt_array_set_low_or_high(rel, key, array,
+									  ScanDirectionIsForward(dir));
+		else
+			_bt_skiparray_set_isnull(rel, key, array);
+	}
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
@@ -2427,17 +2684,25 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Though we advance non-required array keys on our own, that shouldn't have
  * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
+ * have no fixed relationship with the scan's progress.
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass forcenonrequired=true to instruct us to treat all keys as nonrequried.
+ * This is used to make it safe to temporarily stop properly maintaining the
+ * scan's required arrays.  Callers can determine which prefix of keys must
+ * satisfy every possible prefix of index attribute values on the page, and
+ * then pass us an initial *ikey for the first key that might be unsatisified.
+ * We won't be maintaining any arrays before that initial *ikey, so there is
+ * no point in trying to do so for any later arrays.  (Callers that do this
+ * must be careful to reset the array keys when they finish reading the page.)
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool forcenonrequired,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -2454,10 +2719,13 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when we're forced to treat all scan keys as nonrequired)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (forcenonrequired)
+			Assert(!prechecked);
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
@@ -2505,6 +2773,19 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		{
 			Assert(key->sk_flags & SK_SEARCHARRAY);
 			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir || forcenonrequired);
+
+			/*
+			 * Cannot fall back on _bt_tuple_before_array_skeys when we're
+			 * treating the scan's keys as nonrequired, though.  Just handle
+			 * this like any other non-required equality-type array key.
+			 */
+			if (forcenonrequired)
+			{
+				Assert(!(key->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+				return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
+											  tupdesc, *ikey, false);
+			}
 
 			*continuescan = false;
 			return false;
@@ -2514,7 +2795,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
-									 continuescan))
+									 forcenonrequired, continuescan))
 				continue;
 			return false;
 		}
@@ -2546,9 +2827,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			 */
 			if (requiredSameDir)
 				*continuescan = false;
+			else if (unlikely(key->sk_flags & SK_BT_SKIP))
+			{
+				/*
+				 * If we're treating scan keys as nonrequired, and encounter a
+				 * skip array scan key whose current element is NULL, then it
+				 * must be a non-range skip array.  It must be satisfied, so
+				 * there's no need to call _bt_advance_array_keys to check.
+				 */
+				Assert(forcenonrequired && *ikey > 0);
+				continue;
+			}
 
 			/*
-			 * In any case, this indextuple doesn't match the qual.
+			 * This indextuple doesn't match the qual.
 			 */
 			return false;
 		}
@@ -2569,7 +2861,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -2587,7 +2879,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -2656,7 +2948,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
  */
 static bool
 _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
-					 TupleDesc tupdesc, ScanDirection dir, bool *continuescan)
+					 TupleDesc tupdesc, ScanDirection dir,
+					 bool forcenonrequired, bool *continuescan)
 {
 	ScanKey		subkey = (ScanKey) DatumGetPointer(skey->sk_argument);
 	int32		cmpresult = 0;
@@ -2696,7 +2989,11 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 
 		if (isNull)
 		{
-			if (subkey->sk_flags & SK_BT_NULLS_FIRST)
+			if (forcenonrequired)
+			{
+				/* treating scan key as non-required */
+			}
+			else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -2750,8 +3047,12 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			 */
 			Assert(subkey != (ScanKey) DatumGetPointer(skey->sk_argument));
 			subkey--;
-			if ((subkey->sk_flags & SK_BT_REQFWD) &&
-				ScanDirectionIsForward(dir))
+			if (forcenonrequired)
+			{
+				/* treating scan key as non-required */
+			}
+			else if ((subkey->sk_flags & SK_BT_REQFWD) &&
+					 ScanDirectionIsForward(dir))
 				*continuescan = false;
 			else if ((subkey->sk_flags & SK_BT_REQBKWD) &&
 					 ScanDirectionIsBackward(dir))
@@ -2803,7 +3104,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			break;
 	}
 
-	if (!result)
+	if (!result && !forcenonrequired)
 	{
 		/*
 		 * Tuple fails this qual.  If it's a required qual for the current
@@ -2847,6 +3148,8 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	Assert(!pstate->forcenonrequired);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
-- 
2.47.2

v25-0006-DEBUG-Add-skip-scan-disable-GUCs.patchapplication/x-patch; name=v25-0006-DEBUG-Add-skip-scan-disable-GUCs.patchDownload
From aacac27995a29df7a88831279c926ecd21bdd192 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 18 Jan 2025 10:54:44 -0500
Subject: [PATCH v25 6/6] DEBUG: Add skip scan disable GUCs.

---
 src/include/access/nbtree.h                   |  4 +++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 31 +++++++++++++++++++
 src/backend/utils/misc/guc_tables.c           | 23 ++++++++++++++
 3 files changed, 58 insertions(+)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index ef20e9932..565d13fe6 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1184,6 +1184,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 3f47f58f1..b2dfb3eed 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -21,6 +21,27 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 typedef struct BTScanKeyPreproc
 {
 	ScanKey		inkey;
@@ -1636,6 +1657,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
 			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
 
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].sksup = NULL;
+
 			/*
 			 * We'll need a 3-way ORDER proc to perform "binary searches" for
 			 * the next matching array element.  Set that up now.
@@ -2141,6 +2166,12 @@ _bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
 		if (attno_has_rowcompare)
 			break;
 
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
 		/*
 		 * Now consider next attno_inkey (or keep going if this is an
 		 * additional scan key against the same attribute)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 03a6dd491..ae92e8e43 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1783,6 +1784,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3660,6 +3672,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
-- 
2.47.2

v25-0005-Apply-low-order-skip-key-in-_bt_first-more-often.patchapplication/x-patch; name=v25-0005-Apply-low-order-skip-key-in-_bt_first-more-often.patchDownload
From 3d6a886b8043e068640bedb42473fb022b3afdb3 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 13 Jan 2025 16:08:32 -0500
Subject: [PATCH v25 5/6] Apply low-order skip key in _bt_first more often.

Convert low_compare and high_compare nbtree skip array inequalities
(with opclasses that offer skip support) such that _bt_first will always
be able to use any low-order keys in _bt_first.

For example, an index qual "WHERE a > 5 AND b = 2" is now converted to
"WHERE a >= 6 AND b = 2" by a new preprocessing step that takes place
after a final low_compare and/or high_compare are chosen by all earlier
preprocessing steps.  That way the scan's initial call to _bt_first will
use "WHERE a >= 6 AND b = 2" to find the initial leaf level position,
rather than merely using "WHERE a > 5" -- "b = 2" is always included.
This has a decent chance of making the scan avoid an extra _bt_first
call that would otherwise be needed just to determine the lowest-sorting
"a" value in the index (the lowest that still satisfies "WHERE a > 5").

This transformation process can only lower the total number of index
pages read when the use of a more restrictive set of initial positioning
keys in _bt_first actually allows the scan to land on some later leaf
page directly, relative to the unoptimized case (or on an earlier leaf
page directly, when we're scanning backwards).  The savings can be far
greater when affected skip arrays come after some higher-order array.
For example, a qual "WHERE x IN (1, 2, 3) AND y > 5 AND z = 2" can now
save as many as 3 _bt_first calls as a result of these transformations
(there can be as many as 1 _bt_first call saved per "x" array element).

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=FJ78K3WsF3iWNxWnUCY9f=Jdg3QPxaXE=uYUbmuRz5Q@mail.gmail.com
---
 src/backend/access/nbtree/nbtpreprocesskeys.c | 175 ++++++++++++++++++
 src/test/regress/expected/create_index.out    |  21 +++
 src/test/regress/sql/create_index.sql         |  10 +
 3 files changed, 206 insertions(+)

diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 3d8ff701f..3f47f58f1 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -50,6 +50,12 @@ static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
 								 BTArrayKeyInfo *array, bool *qual_ok);
 static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
 								 BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+									   BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
@@ -1288,6 +1294,166 @@ _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
 	return true;
 }
 
+/*
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts their
+ * operator strategies, while changing the operator as appropriate).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value MINVAL, using a transformed >= key
+ * instead of using the original > key makes it safe to include lower-order
+ * scan keys in the insertion scan key (there must be lower-order scan keys
+ * after the skip array).  We will avoid an extra _bt_first to find the first
+ * value in the index > sk_argument -- at least when the first real matching
+ * value in the index happens to be an exact match for the sk_argument value
+ * that we produced here by incrementing the original input key's sk_argument.
+ * (Backwards scans derive the same benefit when they encounter the sentinel
+ * value MAXVAL, by converting the high_compare key from < to <=.)
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01'.
+ */
+static void
+_bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+						   BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	/*
+	 * Called last among all preprocessing steps, when the skip array's final
+	 * low_compare and high_compare have both been chosen
+	 */
+	Assert(so->skipScan);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1 && !array->null_elem && array->sksup);
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skiparray_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skiparray_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1824,6 +1990,15 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && array->sksup &&
+						!array->null_elem)
+						_bt_skiparray_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 15dde752f..fa4517e2d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2589,6 +2589,27 @@ ORDER BY thousand;
         1 |     1001
 (1 row)
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+                                            QUERY PLAN                                            
+--------------------------------------------------------------------------------------------------
+ Limit
+   ->  Index Only Scan using tenk1_thous_tenthous on tenk1
+         Index Cond: ((thousand > '-1'::integer) AND (tenthous = ANY ('{1001,3000}'::integer[])))
+(3 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+        1 |     1001
+(2 rows)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index 40ba3d65b..e8c16959a 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -993,6 +993,16 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
 ORDER BY thousand;
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
-- 
2.47.2

v25-0002-Improve-nbtree-SAOP-primitive-scan-scheduling.patchapplication/x-patch; name=v25-0002-Improve-nbtree-SAOP-primitive-scan-scheduling.patchDownload
From d7b2c11bc8d8426d4c2ed50689cf777037c22994 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Fri, 14 Feb 2025 16:11:59 -0500
Subject: [PATCH v25 2/6] Improve nbtree SAOP primitive scan scheduling.

Add new primitive index scan scheduling heuristics that make
_bt_advance_array_keys avoid ending the ongoing primitive index scan
when it has already read more than one leaf page during the ongoing
primscan.  This tends to result in better decisions about how to start
and end primitive index scans with queries that have SAOP arrays with
many elements that are clustered together (e.g., contiguous integers).

Preparation for an upcoming patch that will add skip scan optimizations
to nbtree.  That'll work by adding skip arrays, which behave similarly
to SAOP arrays with many elements clustered together.

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wzkz0wPe6+02kr+hC+JJNKfGtjGTzpG3CFVTQmKwWNrXNw@mail.gmail.com
---
 src/include/access/nbtree.h           |   9 +-
 src/backend/access/nbtree/nbtsearch.c |  75 +++++----
 src/backend/access/nbtree/nbtutils.c  | 219 ++++++++++++++------------
 3 files changed, 166 insertions(+), 137 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index e4fdeca34..947431954 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1043,8 +1043,8 @@ typedef struct BTScanOpaqueData
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
-	bool		scanBehind;		/* Last array advancement matched -inf attr? */
-	bool		oppositeDirCheck;	/* explicit scanBehind recheck needed? */
+	bool		scanBehind;		/* arrays might be ahead of scan's key space? */
+	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
 	BTArrayKeyInfo *arrayKeys;	/* info about each equality-type array key */
 	FmgrInfo   *orderProcs;		/* ORDER procs for required equality keys */
 	MemoryContext arrayContext; /* scan-lifespan context for array data */
@@ -1099,6 +1099,7 @@ typedef struct BTReadPageState
 	 * Input and output parameters, set and unset by both _bt_readpage and
 	 * _bt_checkkeys to manage precheck optimizations
 	 */
+	bool		firstpage;		/* on first page of current primitive scan? */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
 
@@ -1298,8 +1299,8 @@ extern int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir);
 extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 						  IndexTuple tuple, int tupnatts);
-extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
-								  IndexTuple finaltup);
+extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
+									 IndexTuple finaltup);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 941b4eaaf..babb530ed 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -33,7 +33,7 @@ static OffsetNumber _bt_binsrch(Relation rel, BTScanInsert key, Buffer buf);
 static int	_bt_binsrch_posting(BTScanInsert key, Page page,
 								OffsetNumber offnum);
 static bool _bt_readpage(IndexScanDesc scan, ScanDirection dir,
-						 OffsetNumber offnum, bool firstPage);
+						 OffsetNumber offnum, bool firstpage);
 static void _bt_saveitem(BTScanOpaque so, int itemIndex,
 						 OffsetNumber offnum, IndexTuple itup);
 static int	_bt_setuppostingitems(BTScanOpaque so, int itemIndex,
@@ -1499,7 +1499,7 @@ _bt_next(IndexScanDesc scan, ScanDirection dir)
  */
 static bool
 _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
-			 bool firstPage)
+			 bool firstpage)
 {
 	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -1558,6 +1558,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.offnum = InvalidOffsetNumber;
 	pstate.skip = InvalidOffsetNumber;
 	pstate.continuescan = true; /* default assumption */
+	pstate.firstpage = firstpage;
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
 	pstate.rechecks = 0;
@@ -1603,7 +1604,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!firstPage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
@@ -1620,36 +1621,29 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	if (ScanDirectionIsForward(dir))
 	{
 		/* SK_SEARCHARRAY forward scans must provide high key up front */
-		if (arrayKeys && !P_RIGHTMOST(opaque))
+		if (arrayKeys)
 		{
-			ItemId		iid = PageGetItemId(page, P_HIKEY);
-
-			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
-
-			if (unlikely(so->oppositeDirCheck))
+			if (!P_RIGHTMOST(opaque))
 			{
-				Assert(so->scanBehind);
+				ItemId		iid = PageGetItemId(page, P_HIKEY);
 
-				/*
-				 * Last _bt_readpage call scheduled a recheck of finaltup for
-				 * required scan keys up to and including a > or >= scan key.
-				 *
-				 * _bt_checkkeys won't consider the scanBehind flag unless the
-				 * scan is stopped by a scan key required in the current scan
-				 * direction.  We need this recheck so that we'll notice when
-				 * all tuples on this page are still before the _bt_first-wise
-				 * start of matches for the current set of array keys.
-				 */
-				if (!_bt_oppodir_checkkeys(scan, dir, pstate.finaltup))
+				pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+				/* If a recheck is scheduled, check finaltup first */
+				if (unlikely(so->scanBehind) &&
+					!_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup))
 				{
 					/* Schedule another primitive index scan after all */
 					so->currPos.moreRight = false;
 					so->needPrimScan = true;
+					if (scan->parallel_scan)
+						_bt_parallel_primscan_schedule(scan,
+													   so->currPos.currPage);
 					return false;
 				}
-
-				/* Deliberately don't unset scanBehind flag just yet */
 			}
+
+			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
 		/* load items[] in ascending order */
@@ -1745,7 +1739,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 		 * only appear on non-pivot tuples on the right sibling page are
 		 * common.
 		 */
-		if (pstate.continuescan && !P_RIGHTMOST(opaque))
+		if (pstate.continuescan && !so->scanBehind && !P_RIGHTMOST(opaque))
 		{
 			ItemId		iid = PageGetItemId(page, P_HIKEY);
 			IndexTuple	itup = (IndexTuple) PageGetItem(page, iid);
@@ -1767,11 +1761,29 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	else
 	{
 		/* SK_SEARCHARRAY backward scans must provide final tuple up front */
-		if (arrayKeys && minoff <= maxoff && !P_LEFTMOST(opaque))
+		if (arrayKeys)
 		{
-			ItemId		iid = PageGetItemId(page, minoff);
+			if (minoff <= maxoff && !P_LEFTMOST(opaque))
+			{
+				ItemId		iid = PageGetItemId(page, minoff);
 
-			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+				pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+				/* If a recheck is scheduled, check finaltup first */
+				if (unlikely(so->scanBehind) &&
+					!_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup))
+				{
+					/* Schedule another primitive index scan after all */
+					so->currPos.moreLeft = false;
+					so->needPrimScan = true;
+					if (scan->parallel_scan)
+						_bt_parallel_primscan_schedule(scan,
+													   so->currPos.currPage);
+					return false;
+				}
+			}
+
+			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
 		/* load items[] in descending order */
@@ -2184,7 +2196,9 @@ _bt_readfirstpage(IndexScanDesc scan, OffsetNumber offnum, ScanDirection dir)
  * scan.  A seized=false caller's blkno can never be assumed to be the page
  * that must be read next during a parallel scan, though.  We must figure that
  * part out for ourselves by seizing the scan (the correct page to read might
- * already be beyond the seized=false caller's blkno during a parallel scan).
+ * already be beyond the seized=false caller's blkno during a parallel scan,
+ * unless blkno/so->currPos.nextPage/so->currPos.prevPage is already P_NONE,
+ * or unless so->currPos.moreRight/so->currPos.moreLeft is already unset).
  *
  * On success exit, so->currPos is updated to contain data from the next
  * interesting page, and we return true.  We hold a pin on the buffer on
@@ -2205,6 +2219,7 @@ _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno,
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	Assert(so->currPos.currPage == lastcurrblkno || seized);
+	Assert(!(blkno == P_NONE && seized));
 	Assert(!BTScanPosIsPinned(so->currPos));
 
 	/*
@@ -2272,14 +2287,14 @@ _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno,
 			if (ScanDirectionIsForward(dir))
 			{
 				/* note that this will clear moreRight if we can stop */
-				if (_bt_readpage(scan, dir, P_FIRSTDATAKEY(opaque), false))
+				if (_bt_readpage(scan, dir, P_FIRSTDATAKEY(opaque), seized))
 					break;
 				blkno = so->currPos.nextPage;
 			}
 			else
 			{
 				/* note that this will clear moreLeft if we can stop */
-				if (_bt_readpage(scan, dir, PageGetMaxOffsetNumber(page), false))
+				if (_bt_readpage(scan, dir, PageGetMaxOffsetNumber(page), seized))
 					break;
 				blkno = so->currPos.prevPage;
 			}
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 693e43c67..6a44d293f 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -42,6 +42,8 @@ static bool _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 static bool _bt_verify_arrays_bt_first(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_verify_keys_with_arraykeys(IndexScanDesc scan);
 #endif
+static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
 							  bool advancenonrequired, bool prechecked, bool firstmatch,
@@ -870,15 +872,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	int			arrayidx = 0;
 	bool		beyond_end_advance = false,
 				has_required_opposite_direction_only = false,
-				oppodir_inequality_sktrig = false,
 				all_required_satisfied = true,
 				all_satisfied = true;
 
-	/*
-	 * Unset so->scanBehind (and so->oppositeDirCheck) in case they're still
-	 * set from back when we dealt with the previous page's high key/finaltup
-	 */
-	so->scanBehind = so->oppositeDirCheck = false;
+	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	if (sktrig_required)
 	{
@@ -990,18 +987,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			beyond_end_advance = true;
 			all_satisfied = all_required_satisfied = false;
 
-			/*
-			 * Set a flag that remembers that this was an inequality required
-			 * in the opposite scan direction only, that nevertheless
-			 * triggered the call here.
-			 *
-			 * This only happens when an inequality operator (which must be
-			 * strict) encounters a group of NULLs that indicate the end of
-			 * non-NULL values for tuples in the current scan direction.
-			 */
-			if (unlikely(required_opposite_direction_only))
-				oppodir_inequality_sktrig = true;
-
 			continue;
 		}
 
@@ -1306,10 +1291,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * Note: we don't just quit at this point when all required scan keys were
 	 * found to be satisfied because we need to consider edge-cases involving
 	 * scan keys required in the opposite direction only; those aren't tracked
-	 * by all_required_satisfied. (Actually, oppodir_inequality_sktrig trigger
-	 * scan keys are tracked by all_required_satisfied, since it's convenient
-	 * for _bt_check_compare to behave as if they are required in the current
-	 * scan direction to deal with NULLs.  We'll account for that separately.)
+	 * by all_required_satisfied.
 	 */
 	Assert(_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts,
 										false, 0, NULL) ==
@@ -1365,28 +1347,24 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 *
 	 * You can think of this as a speculative bet on what the scan is likely
 	 * to find on the next page.  It's not much of a gamble, though, since the
-	 * untruncated prefix of attributes must strictly satisfy the new qual
-	 * (though it's okay if any non-required scan keys fail to be satisfied).
+	 * untruncated prefix of attributes must strictly satisfy the new qual.
 	 */
-	if (so->scanBehind && has_required_opposite_direction_only)
+	if (so->scanBehind)
 	{
 		/*
-		 * However, we need to work harder whenever the scan involves a scan
-		 * key required in the opposite direction to the scan only, along with
-		 * a finaltup with at least one truncated attribute that's associated
-		 * with a scan key marked required (required in either direction).
+		 * Truncated high key -- _bt_scanbehind_checkkeys recheck scheduled.
 		 *
-		 * _bt_check_compare simply won't stop the scan for a scan key that's
-		 * marked required in the opposite scan direction only.  That leaves
-		 * us without an automatic way of reconsidering any opposite-direction
-		 * inequalities if it turns out that starting a new primitive index
-		 * scan will allow _bt_first to skip ahead by a great many leaf pages.
-		 *
-		 * We deal with this by explicitly scheduling a finaltup recheck on
-		 * the right sibling page.  _bt_readpage calls _bt_oppodir_checkkeys
-		 * for next page's finaltup (and we skip it for this page's finaltup).
+		 * Remember if recheck needs to call _bt_oppodir_checkkeys for next
+		 * page's finaltup (see below comments about "Handle inequalities
+		 * marked required in the opposite scan direction" for why).
 		 */
-		so->oppositeDirCheck = true;	/* recheck next page's high key */
+		so->oppositeDirCheck = has_required_opposite_direction_only;
+
+		/*
+		 * Make sure that any SAOP arrays that were not marked required by
+		 * preprocessing are reset to their first element for this direction
+		 */
+		_bt_rewind_nonrequired_arrays(scan, dir);
 	}
 
 	/*
@@ -1411,11 +1389,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * (primitive) scan.  If this happens at the start of a large group of
 	 * NULL values, then we shouldn't expect to be called again until after
 	 * the scan has already read indefinitely-many leaf pages full of tuples
-	 * with NULL suffix values.  We need a separate test for this case so that
-	 * we don't miss our only opportunity to skip over such a group of pages.
-	 * (_bt_first is expected to skip over the group of NULLs by applying a
-	 * similar "deduce NOT NULL" rule, where it finishes its insertion scan
-	 * key by consing up an explicit SK_SEARCHNOTNULL key.)
+	 * with NULL suffix values.  (_bt_first is expected to skip over the group
+	 * of NULLs by applying a similar "deduce NOT NULL" rule of its own, which
+	 * involves consing up an explicit SK_SEARCHNOTNULL key.)
 	 *
 	 * Apply a test against finaltup to detect and recover from the problem:
 	 * if even finaltup doesn't satisfy such an inequality, we just skip by
@@ -1423,20 +1399,18 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * that all of the tuples on the current page following caller's tuple are
 	 * also before the _bt_first-wise start of tuples for our new qual.  That
 	 * at least suggests many more skippable pages beyond the current page.
-	 * (when so->oppositeDirCheck was set, this'll happen on the next page.)
+	 * (when so->scanBehind and so->oppositeDirCheck are set, this'll happen
+	 * when we test the next page's finaltup/high key instead.)
 	 */
 	else if (has_required_opposite_direction_only && pstate->finaltup &&
-			 (all_required_satisfied || oppodir_inequality_sktrig) &&
 			 unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup)))
 	{
-		/*
-		 * Make sure that any non-required arrays are set to the first array
-		 * element for the current scan direction
-		 */
 		_bt_rewind_nonrequired_arrays(scan, dir);
 		goto new_prim_scan;
 	}
 
+continue_scan:
+
 	/*
 	 * Stick with the ongoing primitive index scan for now.
 	 *
@@ -1458,8 +1432,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	if (so->scanBehind)
 	{
 		/* Optimization: skip by setting "look ahead" mechanism's offnum */
-		Assert(ScanDirectionIsForward(dir));
-		pstate->skip = pstate->maxoff + 1;
+		if (ScanDirectionIsForward(dir))
+			pstate->skip = pstate->maxoff + 1;
+		else
+			pstate->skip = pstate->minoff - 1;
 	}
 
 	/* Caller's tuple doesn't match the new qual */
@@ -1469,6 +1445,35 @@ new_prim_scan:
 
 	Assert(pstate->finaltup);	/* not on rightmost/leftmost page */
 
+	/*
+	 * Looks like another primitive index scan is required.  But consider
+	 * backing out and continuing the primscan based on scan-level heuristics.
+	 *
+	 * Continue the ongoing primitive scan (but schedule a recheck for when
+	 * the scan arrives on the next sibling leaf page) when it has already
+	 * read at least one leaf page before the one we're reading now.  This is
+	 * important when reading subsets of an index with many distinct values in
+	 * respect of an attribute constrained by an array.  It encourages fewer,
+	 * larger primitive scans where that makes sense.
+	 *
+	 * Note: This heuristic isn't as aggressive as you might think.  We're
+	 * conservative about allowing a primitive scan to step from the first
+	 * leaf page it reads to the page's sibling page (we only allow it on
+	 * first pages whose finaltup strongly suggests that it'll work out).
+	 * Clearing this first page finaltup hurdle is a strong signal in itself.
+	 */
+	if (!pstate->firstpage)
+	{
+		/* Schedule a recheck once on the next (or previous) page */
+		so->scanBehind = true;
+		so->oppositeDirCheck = has_required_opposite_direction_only;
+
+		_bt_rewind_nonrequired_arrays(scan, dir);
+
+		/* Continue the current primitive scan after all */
+		goto continue_scan;
+	}
+
 	/*
 	 * End this primitive index scan, but schedule another.
 	 *
@@ -1499,7 +1504,7 @@ end_toplevel_scan:
 	 * first positions for what will then be the current scan direction.
 	 */
 	pstate->continuescan = false;	/* Tell _bt_readpage we're done... */
-	so->needPrimScan = false;	/* ...don't call _bt_first again, though */
+	so->needPrimScan = false;	/* ...and don't call _bt_first again */
 
 	/* Caller's tuple doesn't match any qual */
 	return false;
@@ -1634,6 +1639,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
+	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
 							arrayKeys, pstate->prechecked, pstate->firstmatch,
@@ -1688,62 +1694,36 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
+		/* Override _bt_check_compare, continue primitive scan */
+		pstate->continuescan = true;
+
 		/*
-		 * Tuple is still before the start of matches according to the scan's
-		 * required array keys (according to _all_ of its required equality
-		 * strategy keys, actually).
+		 * We will end up here repeatedly given a group of tuples > the
+		 * previous array keys and < the now-current keys (for a backwards
+		 * scan it's just the same, though the operators swap positions).
 		 *
-		 * _bt_advance_array_keys occasionally sets so->scanBehind to signal
-		 * that the scan's current position/tuples might be significantly
-		 * behind (multiple pages behind) its current array keys.  When this
-		 * happens, we need to be prepared to recover by starting a new
-		 * primitive index scan here, on our own.
+		 * We must avoid allowing this linear search process to scan very many
+		 * tuples from well before the start of tuples matching the current
+		 * array keys (or from well before the point where we'll once again
+		 * have to advance the scan's array keys).
+		 *
+		 * We keep the overhead under control by speculatively "looking ahead"
+		 * to later still-unscanned items from this same leaf page. We'll only
+		 * attempt this once the number of tuples that the linear search
+		 * process has examined starts to get out of hand.
 		 */
-		Assert(!so->scanBehind ||
-			   so->keyData[ikey].sk_strategy == BTEqualStrategyNumber);
-		if (unlikely(so->scanBehind) && pstate->finaltup &&
-			_bt_tuple_before_array_skeys(scan, dir, pstate->finaltup, tupdesc,
-										 BTreeTupleGetNAtts(pstate->finaltup,
-															scan->indexRelation),
-										 false, 0, NULL))
+		pstate->rechecks++;
+		if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
 		{
-			/* Cut our losses -- start a new primitive index scan now */
-			pstate->continuescan = false;
-			so->needPrimScan = true;
-		}
-		else
-		{
-			/* Override _bt_check_compare, continue primitive scan */
-			pstate->continuescan = true;
+			/* See if we should skip ahead within the current leaf page */
+			_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
 
 			/*
-			 * We will end up here repeatedly given a group of tuples > the
-			 * previous array keys and < the now-current keys (for a backwards
-			 * scan it's just the same, though the operators swap positions).
-			 *
-			 * We must avoid allowing this linear search process to scan very
-			 * many tuples from well before the start of tuples matching the
-			 * current array keys (or from well before the point where we'll
-			 * once again have to advance the scan's array keys).
-			 *
-			 * We keep the overhead under control by speculatively "looking
-			 * ahead" to later still-unscanned items from this same leaf page.
-			 * We'll only attempt this once the number of tuples that the
-			 * linear search process has examined starts to get out of hand.
+			 * Might have set pstate.skip to a later page offset.  When that
+			 * happens then _bt_readpage caller will inexpensively skip ahead
+			 * to a later tuple from the same page (the one just after the
+			 * tuple we successfully "looked ahead" to).
 			 */
-			pstate->rechecks++;
-			if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
-			{
-				/* See if we should skip ahead within the current leaf page */
-				_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
-
-				/*
-				 * Might have set pstate.skip to a later page offset.  When
-				 * that happens then _bt_readpage caller will inexpensively
-				 * skip ahead to a later tuple from the same page (the one
-				 * just after the tuple we successfully "looked ahead" to).
-				 */
-			}
 		}
 
 		/* This indextuple doesn't match the current qual, in any case */
@@ -1760,6 +1740,39 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 								  ikey, true);
 }
 
+/*
+ * Test whether finaltup (the final tuple on the page) is still before the
+ * start of matches for the current array keys.
+ *
+ * Caller's finaltup tuple is the page high key (for forwards scans), or the
+ * first non-pivot tuple (for backwards scans).  Called during scans with
+ * array keys when the so->scanBehind flag was set on the previous page.
+ *
+ * Returns false if the tuple is still before the start of matches.  When that
+ * happens caller should cut its losses and start a new primitive index scan.
+ * Otherwise returns true.
+ */
+bool
+_bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
+						 IndexTuple finaltup)
+{
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	int			nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+
+	Assert(so->numArrayKeys);
+
+	if (_bt_tuple_before_array_skeys(scan, dir, finaltup, tupdesc,
+									 nfinaltupatts, false, 0, NULL))
+		return false;
+
+	if (!so->oppositeDirCheck)
+		return true;
+
+	return _bt_oppodir_checkkeys(scan, dir, finaltup);
+}
+
 /*
  * Test whether an indextuple fails to satisfy an inequality required in the
  * opposite direction only.
@@ -1778,7 +1791,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
  * _bt_checkkeys to stop the scan to consider array advancement/starting a new
  * primitive index scan.
  */
-bool
+static bool
 _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 					  IndexTuple finaltup)
 {
-- 
2.47.2

v25-0003-Add-nbtree-skip-scan-optimizations.patchapplication/x-patch; name=v25-0003-Add-nbtree-skip-scan-optimizations.patchDownload
From c19237253c28a2ee161662306fc36ad3c7eeec5a Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v25 3/6] Add nbtree skip scan optimizations.

Teach nbtree composite index scans to opportunistically skip over
irrelevant sections of composite indexes given a query with an omitted
prefix column.  When nbtree is passed input scan keys derived from a
query predicate "WHERE b = 5", new nbtree preprocessing steps now output
"WHERE a = ANY(<every possible 'a' value>) AND b = 5" scan keys.  That
is, preprocessing generates a "skip array" (along with an associated
scan key) for the omitted column "a", which makes it safe to mark the
scan key on "b" as required to continue the scan.  This is far more
efficient than a traditional full index scan whenever it allows the scan
to skip over many irrelevant leaf pages, by iteratively repositioning
itself using the keys on "a" and "b" together.

A skip array has "elements" that are generated procedurally and on
demand, but otherwise work just like conventional ScalarArrayOp arrays.
They advance using the same approach as ScalarArrayOp arrays, which
allows nbtree scans to freely add skip arrays either before or after any
ScalarArrayOp array.  Index scans decide when and where to reposition
the scan using the same primitive index scan scheduling logic introduced
by commit 5bf748b8 (which enhanced nbtree ScalarArrayOp execution).

The core B-Tree operator classes on most discrete types generate their
array elements with help from their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the very next
indexable value frequently turns out to be the next indexed value.  In
practice, this is likely whenever the scan skips with an input opclass
where "dense" indexed values occur naturally, such as btree/date_ops.
Opclasses that lack a skip support routine fall back on having nbtree
"increment" (or "decrement") a skip array's current element by setting
the NEXT (or PRIOR) scan key flag, without directly changing the scan
key's sk_argument.  These sentinel values behave just like any other
value that might appear in an array -- though they can never actually
locate matching index tuples.

Inequality scan keys can affect how skip arrays generate their values.
Their range is constrained by the inequalities.  For example, a skip
array on "a" will only ever use element values 1 and 2 given a qual
"WHERE a BETWEEN 1 AND 2 AND b = 66".  A scan using such a skip array
has almost identical performance characteristics to a scan with the qual
"WHERE a = ANY('{1, 2}') AND b = 66".  The scan will be much faster when
it can be executed as two selective primitive index scans, rather than a
single very large index scan that has to read many more leaf pages.  It
isn't guaranteed to improve performance, though.

B-Tree preprocessing is optimistic about skipping working out: it
applies static, generic rules when determining where to generate skip
arrays, which assumes that the runtime overhead of maintaining skip
arrays will pay for itself -- or lead to only a modest performance loss.
As things stand, these assumptions are much too optimistic: skip array
maintenance will lead to unacceptable regressions with unsympathetic
queries (typically scans with little to no potential for the scan to
skip over irrelevant index leaf pages).  An upcoming commit will address
the problems in this area separately, by adding a new mechanism that
greatly lowers the cost of array maintenance in these unfavorable cases.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |   3 +-
 src/include/access/nbtree.h                   |  35 +-
 src/include/catalog/pg_amproc.dat             |  22 +
 src/include/catalog/pg_proc.dat               |  27 +
 src/include/storage/lwlock.h                  |   1 +
 src/include/utils/skipsupport.h               |  98 +++
 src/backend/access/index/indexam.c            |   3 +-
 src/backend/access/nbtree/nbtcompare.c        | 273 +++++++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 601 ++++++++++++--
 src/backend/access/nbtree/nbtree.c            | 211 ++++-
 src/backend/access/nbtree/nbtsearch.c         | 111 ++-
 src/backend/access/nbtree/nbtutils.c          | 748 ++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |   4 +
 src/backend/commands/opclasscmds.c            |  25 +
 src/backend/storage/lmgr/lwlock.c             |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/date.c                  |  46 ++
 src/backend/utils/adt/meson.build             |   1 +
 src/backend/utils/adt/selfuncs.c              | 405 +++++++---
 src/backend/utils/adt/skipsupport.c           |  61 ++
 src/backend/utils/adt/timestamp.c             |  48 ++
 src/backend/utils/adt/uuid.c                  |  70 ++
 doc/src/sgml/btree.sgml                       |  34 +-
 doc/src/sgml/indexam.sgml                     |   3 +-
 doc/src/sgml/indices.sgml                     |  49 +-
 doc/src/sgml/monitoring.sgml                  |   5 +-
 doc/src/sgml/xindex.sgml                      |  16 +-
 src/test/regress/expected/alter_generic.out   |  10 +-
 src/test/regress/expected/btree_index.out     |  41 +
 src/test/regress/expected/create_index.out    | 183 ++++-
 src/test/regress/expected/psql.out            |   3 +-
 src/test/regress/sql/alter_generic.sql        |   5 +-
 src/test/regress/sql/btree_index.sql          |  21 +
 src/test/regress/sql/create_index.sql         |  63 +-
 src/tools/pgindent/typedefs.list              |   3 +
 36 files changed, 2864 insertions(+), 368 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index fbe6b225e..09aeab3e8 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -214,7 +214,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 947431954..10d9f8186 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -707,6 +708,10 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
  *	(BTOPTIONS_PROC).  These procedures define a set of user-visible
  *	parameters that can be used to control operator class behavior.  None of
  *	the built-in B-Tree operator classes currently register an "options" proc.
+ *
+ *	To facilitate more efficient B-Tree skip scans, an operator class may
+ *	choose to offer a sixth amproc procedure (BTSKIPSUPPORT_PROC).  For full
+ *	details, see src/include/utils/skipsupport.h.
  */
 
 #define BTORDER_PROC		1
@@ -714,7 +719,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1027,10 +1033,21 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
-	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+	int			cur_elem;		/* index of current element in elem_values */
+
+	/* fields for skip arrays, which generate element datums procedurally */
+	int16		attlen;			/* attr len in bytes */
+	bool		attbyval;		/* as FormData_pg_attribute.attbyval */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupport sksup;			/* skip support (NULL if opclass lacks it) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1042,6 +1059,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* arrays might be ahead of scan's key space? */
 	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
@@ -1119,6 +1137,15 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on column without input = */
+
+/* SK_BT_SKIP-only flags (mutable, set and unset by array advancement) */
+#define SK_BT_MINVAL	0x00080000	/* invalid sk_argument, use low_compare */
+#define SK_BT_MAXVAL	0x00100000	/* invalid sk_argument, use high_compare */
+#define SK_BT_NEXT		0x00200000	/* positions the scan > sk_argument */
+#define SK_BT_PRIOR		0x00400000	/* positions the scan < sk_argument */
+
+/* Remaps pg_index flag bits to uppermost SK_BT_* byte */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1165,7 +1192,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 19100482b..37000639f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e2d5c0d08..783c67eaf 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2266,6 +2281,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4456,6 +4474,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6328,6 +6349,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9376,6 +9400,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 13a7dc899..ffa03189e 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -194,6 +194,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..bc51847cf
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining groups of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * It usually isn't feasible to implement skip support for an opclass whose
+ * input type is continuous.  The B-Tree code falls back on next-key sentinel
+ * values for any opclass that doesn't provide its own skip support function.
+ * This isn't really an implementation restriction; there is no benefit to
+ * providing skip support for an opclass where guessing that the next indexed
+ * value is the next possible indexable value never (or hardly ever) works out.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order)
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 * Operator class decrement/increment functions will never be called with
+	 * a NULL "existing" argument, either.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern SkipSupport PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+												 bool reverse);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 8b1f55543..0e62d7a7a 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 291cb8fc1..4da5a3c1d 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 1fd1da5f1..3d8ff701f 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -45,8 +45,15 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
+static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
+								 ScanKey skey, FmgrInfo *orderproc,
+								 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
+								 BTArrayKeyInfo *array, bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
+							   int *numSkipArrayKeys);
 static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
 									  Oid elemtype, StrategyNumber strat,
 									  Datum *elems, int nelems);
@@ -89,6 +96,8 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -101,10 +110,16 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never generated skip array scan keys, it would be possible for "gaps"
+ * to appear that make it unsafe to mark any subsequent input scan keys
+ * (copied from scan->keyData[]) as required to continue the scan.  Prior to
+ * Postgres 18, a qual like "WHERE y = 4" always required a full index scan.
+ * It'll now become "WHERE x = ANY('{every possible x value}') and y = 4" on
+ * output, due to the addition of a skip array on "x".  This has the potential
+ * to be much more efficient than a full index scan (though it'll behave like
+ * a full index scan when there are many distinct values for "x").
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -141,7 +156,12 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -200,6 +220,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProcs[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -231,6 +259,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
+		Assert(!so->skipScan);
 
 		return;
 	}
@@ -288,7 +317,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -345,7 +375,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -393,6 +422,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, _bt_preprocess_array_keys has already added "=" keys
+			 * sufficient to form an unbroken series of "=" constraints on all
+			 * attrs prior to the attr from the final scan->keyData[] key.
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -481,6 +515,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -489,6 +524,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -508,8 +544,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -803,6 +837,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -812,6 +849,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -885,6 +938,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -978,24 +1032,55 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
  * Compare an array scan key to a scalar scan key, eliminating contradictory
  * array elements such that the scalar scan key becomes redundant.
  *
+ * If the opfamily is incomplete we may not be able to determine which
+ * elements are contradictory.  When we return true we'll have validly set
+ * *qual_ok, guaranteeing that at least the scalar scan key can be considered
+ * redundant.  We return false if the comparison could not be made (caller
+ * must keep both scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar scan keys.
+ */
+static bool
+_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
+							   bool *qual_ok)
+{
+	Assert(arraysk->sk_attno == skey->sk_attno);
+	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
+		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
+	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
+		   skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Just call the appropriate helper function based on whether it's a SAOP
+	 * array or a skip array.  Both helpers will set *qual_ok in passing.
+	 */
+	if (array->num_elems != -1)
+		return _bt_saoparray_shrink(scan, arraysk, skey, orderproc, array,
+									qual_ok);
+	else
+		return _bt_skiparray_shrink(scan, skey, array, qual_ok);
+}
+
+/*
+ * Preprocessing of SAOP (non-skip) array scan key, used to determine which
+ * array elements are eliminated as contradictory by a non-array scalar key.
+ * _bt_compare_array_scankey_args helper function.
+ *
  * Array elements can be eliminated as contradictory when excluded by some
  * other operator on the same attribute.  For example, with an index scan qual
  * "WHERE a IN (1, 2, 3) AND a < 2", all array elements except the value "1"
  * are eliminated, and the < scan key is eliminated as redundant.  Cases where
  * every array element is eliminated by a redundant scalar scan key have an
  * unsatisfiable qual, which we handle by setting *qual_ok=false for caller.
- *
- * If the opfamily doesn't supply a complete set of cross-type ORDER procs we
- * may not be able to determine which elements are contradictory.  If we have
- * the required ORDER proc then we return true (and validly set *qual_ok),
- * guaranteeing that at least the scalar scan key can be considered redundant.
- * We return false if the comparison could not be made (caller must keep both
- * scan keys when this happens).
  */
 static bool
-_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
-							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
-							   bool *qual_ok)
+_bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+					 FmgrInfo *orderproc, BTArrayKeyInfo *array, bool *qual_ok)
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
@@ -1006,14 +1091,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
-	Assert(arraysk->sk_attno == skey->sk_attno);
 	Assert(array->num_elems > 0);
-	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
-		   arraysk->sk_strategy == BTEqualStrategyNumber);
-	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
-		   skey->sk_strategy != BTEqualStrategyNumber);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
 
 	/*
 	 * _bt_binsrch_array_skey searches an array for the entry best matching a
@@ -1112,6 +1191,103 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	return true;
 }
 
+/*
+ * Preprocessing of skip (non-SAOP) array scan key, used to determine
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function.
+ *
+ * Unlike _bt_saoparray_shrink, we don't modify caller's array in-place.  Skip
+ * arrays work by procedurally generating their elements as needed, so we just
+ * store the inequality as the skip array's low_compare or high_compare.  The
+ * array's elements will be generated from the range of values that satisfies
+ * both low_compare and high_compare.
+ */
+static bool
+_bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
+					 bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Array's index attribute will be constrained by a strict operator/key.
+	 * Array must not "contain a NULL element" (i.e. the scan must not apply
+	 * "IS NULL" qual when it reaches the end of the index that stores NULLs).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Consider if we should treat caller's scalar scan key as the skip
+	 * array's high_compare or low_compare.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way the scan can always safely assume that
+	 * it's okay to use the input-opclass-only-type proc from so->orderProcs[]
+	 * (they can be cross-type with SAOP arrays, but never with skip arrays).
+	 *
+	 * This approach is enabled by MINVAL/MAXVAL sentinel key markings, which
+	 * can be thought of as representing either the lowest or highest matching
+	 * array element (excluding the NULL element, where applicable, though as
+	 * just discussed it isn't applicable to this range skip array anyway).
+	 * Array keys marked MINVAL/MAXVAL never has a valid datum stored in its
+	 * sk_argument.  The scan must directly apply the array's low_compare when
+	 * it encounters MINVAL (or its high_compare when it encounters MAXVAL),
+	 * and must never use the array's so->orderProcs[] proc against
+	 * low_compare's/high_compare's sk_argument.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* replace existing high_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing high_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's high_compare */
+			array->high_compare = skey;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* replace existing low_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing low_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's low_compare */
+			array->low_compare = skey;
+			break;
+		case BTEqualStrategyNumber:
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1137,6 +1313,12 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -1152,41 +1334,36 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	Oid			skip_eq_ops[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
-	ScanKey		cur;
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
+	so->skipScan = (numSkipArrayKeys > 0);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys we'll need to generate below
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -1201,18 +1378,20 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
+		ScanKey		inkey = scan->keyData + input_ikey,
+					cur;
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
 		Oid			elemtype;
@@ -1225,21 +1404,114 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		Datum	   *elem_values;
 		bool	   *elem_nulls;
 		int			num_nonnulls;
-		int			j;
+
+		/* set up next output scan key */
+		cur = &arrayKeyData[numArrayKeyData];
+
+		/* Backfill skip arrays for attrs < or <= input key's attr? */
+		while (numSkipArrayKeys && attno_skip <= inkey->sk_attno)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skip_eq_ops[attno_skip - 1];
+			CompactAttribute *attr;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/*
+				 * Attribute already has an = input key, so don't output a
+				 * skip array for attno_skip.  Just copy attribute's = input
+				 * key into arrayKeyData[] once outside this inner loop.
+				 *
+				 * Note: When we get here there must be a later attribute that
+				 * lacks an equality input key, and still needs a skip array
+				 * (if there wasn't then numSkipArrayKeys would be 0 by now).
+				 */
+				Assert(attno_skip == inkey->sk_attno);
+				/* inkey can't be last input key to be marked required: */
+				Assert(input_ikey < scan->numberOfKeys - 1);
+#if 0
+				/* Could be a redundant input scan key, so can't do this: */
+				Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+					   (inkey->sk_flags & SK_SEARCHNULL));
+#endif
+
+				attno_skip++;
+				break;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize generic BTArrayKeyInfo fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+
+			/* Initialize skip array specific BTArrayKeyInfo fields */
+			attr = TupleDescCompactAttr(RelationGetDescr(rel), attno_skip - 1);
+			reverse = (indoption[attno_skip - 1] & INDOPTION_DESC) != 0;
+			so->arrayKeys[numArrayKeys].attlen = attr->attlen;
+			so->arrayKeys[numArrayKeys].attbyval = attr->attbyval;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup =
+				PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse);
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * We'll need a 3-way ORDER proc to perform "binary searches" for
+			 * the next matching array element.  Set that up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			numArrayKeys++;
+			numArrayKeyData++;	/* keep this scan key/array */
+
+			/* set up next output scan key */
+			cur = &arrayKeyData[numArrayKeyData];
+
+			/* remember having output this skip array and scan key */
+			numSkipArrayKeys--;
+			attno_skip++;
+		}
 
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
-		*cur = scan->keyData[input_ikey];
+		*cur = *inkey;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -1257,7 +1529,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * all btree operators are strict.
 		 */
 		num_nonnulls = 0;
-		for (j = 0; j < num_elems; j++)
+		for (int j = 0; j < num_elems; j++)
 		{
 			if (!elem_nulls[j])
 				elem_values[num_nonnulls++] = elem_values[j];
@@ -1295,7 +1567,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -1306,7 +1578,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -1323,7 +1595,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -1392,23 +1664,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			origelemtype = elemtype;
 		}
 
-		/*
-		 * And set up the BTArrayKeyInfo data.
-		 *
-		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
-		 * scan_key field later on, after so->keyData[] has been finalized.
-		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		/* Initialize generic BTArrayKeyInfo fields */
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
+
+		/* Initialize SAOP array specific BTArrayKeyInfo fields */
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].cur_elem = -1;	/* i.e. invalid */
+
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
+	Assert(numSkipArrayKeys == 0);
+
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -1514,7 +1787,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -1565,7 +1839,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -1575,6 +1849,189 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit every so->arrayKeys[] entry.
+ *
+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller must add this many
+ * skip arrays to each of the most significant attributes lacking any keys
+ * that use the = strategy (IS NULL keys count as = keys here).  The specific
+ * attributes that need skip arrays are indicated by initializing caller's
+ * skip_eq_ops[] 0-based attribute offset to a valid = op strategy Oid.  We'll
+ * only ever set skip_eq_ops[] entries to InvalidOid for attributes that
+ * already have an equality key in scan->keyData[] input keys -- and only when
+ * there's some later "attribute gap" for us to "fill-in" with a skip array.
+ *
+ * We're optimistic about skipping working out: we always add exactly the skip
+ * arrays needed to maximize the number of input scan keys that can ultimately
+ * be marked as required to continue the scan (but no more).  For a composite
+ * index on (a, b, c, d), we'll instruct caller to add skip arrays as follows:
+ *
+ * Input keys						Output keys (after all preprocessing)
+ * ----------						-------------------------------------
+ * a = 1							a = 1 (no skip arrays)
+ * a = ANY('{0, 1}')				a = ANY('{0, 1}') (no skip arrays)
+ * b = 42							skip a AND b = 42
+ * b = ANY('{40, 42}')				skip a AND b = ANY('{40, 42}')
+ * a = 1 AND b = 42					a = 1 AND b = 42 (no skip arrays)
+ * a >= 1 AND b = 42				range skip a AND b = 42
+ * a = 1 AND b > 42					a = 1 AND b > 42 (no skip arrays)
+ * a >= 1 AND a <= 3 AND b = 42		range skip a AND b = 42
+ * a = 1 AND c <= 27				a = 1 AND skip b AND c <= 27
+ * a = 1 AND d >= 1					a = 1 AND skip b AND skip c AND d >= 1
+ * a = 1 AND b >= 42 AND d > 1		a = 1 AND range skip b AND skip c AND d > 1
+ */
+static int
+_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_skip = 1,
+				attno_inkey = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+#ifdef DEBUG_DISABLE_SKIP_SCAN
+	/* don't attempt to add skip arrays */
+	return numArrayKeys;
+#endif
+
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+			/* Look up input opclass's equality operator (might fail) */
+			skip_eq_ops[attno_skip - 1] =
+				get_opfamily_member(opfamily, opcintype, opcintype,
+									BTEqualStrategyNumber);
+			if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key
+		 * (doesn't matter whether that attribute has an equality key, or just
+		 * uses inequality keys).
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys
+			 */
+			if (attno_has_equal)
+			{
+				/* Attributes with an = key must have InvalidOid eq_op set */
+				skip_eq_ops[attno_skip - 1] = InvalidOid;
+			}
+			else
+			{
+				Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+				Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+				/* Look up input opclass's equality operator (might fail) */
+				skip_eq_ops[attno_skip - 1] =
+					get_opfamily_member(opfamily, opcintype, opcintype,
+										BTEqualStrategyNumber);
+
+				if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+				{
+					/*
+					 * Input opclass lacks an equality strategy operator, so
+					 * don't generate a skip array that definitely won't work
+					 */
+					break;
+				}
+
+				/* plan on adding a backfill skip array for this attribute */
+				(*numSkipArrayKeys)++;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required later on.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
 /*
  * _bt_find_extreme_element() -- get least or greatest array element
  *
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index fe8578eed..aae2a46c8 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -30,6 +30,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -71,7 +72,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -79,11 +80,24 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 *
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -333,6 +347,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	else
 		so->keyData = NULL;
 
+	so->skipScan = false;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->oppositeDirCheck = false;
@@ -538,10 +553,155 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume all input scankeys will be output with its own
+	 * SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * scan array (and associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		CompactAttribute *attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescCompactAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   array->attbyval, array->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array key, while freeing memory allocated for old
+		 * sk_argument where required
+		 */
+		if (!array->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -552,7 +712,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -575,16 +736,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -611,6 +772,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -655,7 +817,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -677,14 +839,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -722,7 +879,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -766,11 +923,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -809,7 +966,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -822,7 +979,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * primscan counter, which we maintain in shared memory
 	 */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -840,6 +997,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -849,7 +1007,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -859,14 +1017,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index babb530ed..3e06a260e 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -964,6 +964,15 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  All
+	 * array keys are always considered = keys, but we'll sometimes need to
+	 * treat the current key value as if we were using an inequality strategy.
+	 * This happens whenever a skip array's scan key is found to have been set
+	 * to one of the special sentinel values representing an imaginary point
+	 * before/after some (or all) of the index's real indexable values.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1039,8 +1048,56 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is MINVAL, choose low_compare (when scanning
+				 * backwards it'll be MAXVAL, and we'll choose high_compare).
+				 *
+				 * Note: if the array's low_compare key makes 'chosen' NULL,
+				 * then we behave as if the array's first element is -inf,
+				 * except when !array->null_elem implies a usable NOT NULL
+				 * constraint.
+				 */
+				if (chosen != NULL &&
+					(chosen->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skipequalitykey = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					Assert(so->skipScan);
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MAXVAL));
+						chosen = array->low_compare;
+					}
+					else
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MINVAL));
+						chosen = array->high_compare;
+					}
+
+					Assert(chosen == NULL ||
+						   chosen->sk_attno == skipequalitykey->sk_attno);
+
+					if (!array->null_elem)
+						impliesNN = skipequalitykey;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1083,9 +1140,41 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 
 				/*
-				 * Done if that was the last attribute, or if next key is not
-				 * in sequence (implying no boundary key is available for the
-				 * next attribute).
+				 * If the key that we just added to startKeys[] is a skip
+				 * array = key whose current element is marked NEXT or PRIOR,
+				 * make strat_total > or < (and stop adding boundary keys).
+				 * This can only happen with opclasses that lack skip support.
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(so->skipScan);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(strat_total == BTEqualStrategyNumber);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We're done.  We'll never find an exact = match for a
+					 * NEXT or PRIOR sentinel sk_argument value.  There's no
+					 * sense in trying to add more keys to startKeys[].
+					 */
+					break;
+				}
+
+				/*
+				 * Done if that was the last scan key output by preprocessing.
+				 * Also done if there is a gap index attribute that lacks a
+				 * usable key (only possible when preprocessing was unable to
+				 * generate a skip array key to "fill in the gap").
 				 */
 				if (i >= so->numberOfKeys ||
 					cur->sk_attno != curattr + 1)
@@ -1577,10 +1666,12 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * corresponding value from the last item on the page.  So checking with
 	 * the last item on the page would give a more precise answer.
 	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
+	 * We don't do this for the first page read by each (primitive) scan, to
+	 * avoid slowing down point queries.  They typically don't stand to gain
+	 * much when the optimization can be applied, and are more likely to
+	 * notice the overhead of the precheck.  Also avoid it during skip scans,
+	 * where individual primitive scans that don't just read one leaf page are
+	 * likely to advance their skip array(s) several times per leaf page read.
 	 *
 	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
 	 * just set a low-order required array's key to the best available match
@@ -1604,7 +1695,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !so->skipScan && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 6a44d293f..b45e3d32a 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -30,6 +30,17 @@
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									  int32 set_elem_result, Datum tupdatum, bool tupnull);
+static void _bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_array_set_low_or_high(Relation rel, ScanKey skey,
+									  BTArrayKeyInfo *array, bool low_not_high);
+static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -207,6 +218,7 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 	int32		result = 0;
 
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
+	Assert(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)));
 
 	if (tupnull)				/* NULL tupdatum */
 	{
@@ -283,6 +295,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* plain arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -405,6 +419,185 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, since skip arrays don't really
+ * contain elements (they generate their array elements procedurally instead).
+ * Our interface matches that of _bt_binsrch_array_skey in every other way.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  We return -1 when tupdatum/tupnull is lower that any value
+ * within the range of the array, and 1 when it is higher than every value.
+ * Caller should pass *set_elem_result to _bt_skiparray_set_element to advance
+ * the array.
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (array->null_elem)
+	{
+		Assert(!array->low_compare && !array->high_compare);
+
+		*set_elem_result = 0;
+		return;
+	}
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+
+	/*
+	 * Assert that any keys that were assumed to be satisfied already (due to
+	 * caller passing cur_elem_trig=true) really are satisfied as expected
+	 */
+#ifdef USE_ASSERT_CHECKING
+	if (cur_elem_trig)
+	{
+		if (ScanDirectionIsForward(dir) && array->low_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												  array->low_compare->sk_collation,
+												  tupdatum,
+												  array->low_compare->sk_argument)));
+
+		if (ScanDirectionIsBackward(dir) && array->high_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												  array->high_compare->sk_collation,
+												  tupdatum,
+												  array->high_compare->sk_argument)));
+	}
+#endif
+}
+
+/*
+ * _bt_skiparray_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Caller passes set_elem_result returned by _bt_binsrch_skiparray_skey for
+ * caller's tupdatum/tupnull.
+ *
+ * We copy tupdatum/tupnull into skey's sk_argument iff set_elem_result == 0.
+ * Otherwise, we set skey to either the lowest or highest value that's within
+ * the range of caller's skip array (whichever is the best available match to
+ * tupdatum/tupnull that is still within the range of the skip array according
+ * to _bt_binsrch_skiparray_skey/set_elem_result).
+ */
+static void
+_bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  int32 set_elem_result, Datum tupdatum, bool tupnull)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (set_elem_result)
+	{
+		/* tupdatum/tupnull is out of the range of the skip array */
+		Assert(!array->null_elem);
+
+		_bt_array_set_low_or_high(rel, skey, array, set_elem_result < 0);
+		return;
+	}
+
+	/* Advance skip array to tupdatum (or tupnull) value */
+	if (unlikely(tupnull))
+	{
+		_bt_skiparray_set_isnull(rel, skey, array);
+		return;
+	}
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen);
+}
+
+/*
+ * _bt_skiparray_set_isnull() -- set skip array scan key to NULL
+ */
+static void
+_bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->null_elem && !array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -414,29 +607,349 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_array_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_array_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  bool low_not_high)
+{
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting array to lowest element (according to low_compare) */
+		skey->sk_flags |= SK_BT_MINVAL;
+	}
+	else
+	{
+		/* Setting array to highest element (according to high_compare) */
+		skey->sk_flags |= SK_BT_MAXVAL;
+	}
+}
+
+/*
+ * _bt_array_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the minimum value within the range
+	 * of a skip array (often just -inf) is never decrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly decrement sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Decrement" from NULL to the high_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->high_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup->decrement(rel, skey->sk_argument, &uflow);
+	if (unlikely(uflow))
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_skiparray_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_array_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MINVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the maximum value within the range
+	 * of a skip array (often just +inf) is never incrementable
+	 */
+	if (skey->sk_flags & SK_BT_MAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly increment sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Increment" from NULL to the low_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->low_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup->increment(rel, skey->sk_argument, &oflow);
+	if (unlikely(oflow))
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_skiparray_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -452,6 +965,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -461,29 +975,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_array_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_array_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -507,7 +1022,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 }
 
 /*
- * _bt_rewind_nonrequired_arrays() -- Rewind non-required arrays
+ * _bt_rewind_nonrequired_arrays() -- Rewind SAOP arrays not marked required
  *
  * Called when _bt_advance_array_keys decides to start a new primitive index
  * scan on the basis of the current scan position being before the position
@@ -539,10 +1054,15 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
  *
  * Note: _bt_verify_arrays_bt_first is called by an assertion to enforce that
  * everybody got this right.
+ *
+ * Note: In practice almost all SAOP arrays are marked required during
+ * preprocessing (if necessary by generating skip arrays).  It is hardly ever
+ * truly necessary to call here, but consistently doing so is simpler.
  */
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -550,7 +1070,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -562,16 +1081,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_array_set_low_or_high(rel, cur, array,
+								  ScanDirectionIsForward(dir));
 	}
 }
 
@@ -696,9 +1209,77 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (likely(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))))
+		{
+			/* Scankey has a valid/comparable sk_argument value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0)
+			{
+				/*
+				 * Interpret result in a way that takes NEXT/PRIOR into
+				 * account
+				 */
+				if (cur->sk_flags & SK_BT_NEXT)
+					result = -1;
+				else if (cur->sk_flags & SK_BT_PRIOR)
+					result = 1;
+
+				Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP));
+			}
+		}
+		else
+		{
+			BTArrayKeyInfo *array = NULL;
+
+			/*
+			 * Current array element/array = scan key value is a sentinel
+			 * value that represents the lowest (or highest) possible value
+			 * that's still within the range of the array.
+			 *
+			 * Like _bt_first, we only see MINVAL keys during forwards scans
+			 * (and similarly only see MAXVAL keys during backwards scans).
+			 * Even if the scan's direction changes, we'll stop at some higher
+			 * order key before we can ever reach any MAXVAL (or MINVAL) keys.
+			 * (However, unlike _bt_first we _can_ get to keys marked either
+			 * NEXT or PRIOR, regardless of the scan's current direction.)
+			 */
+			Assert(ScanDirectionIsForward(dir) ?
+				   !(cur->sk_flags & SK_BT_MAXVAL) :
+				   !(cur->sk_flags & SK_BT_MINVAL));
+
+			/*
+			 * There are no valid sk_argument values in MINVAL/MAXVAL keys.
+			 * Check if tupdatum is within the range of skip array instead.
+			 */
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			_bt_binsrch_skiparray_skey(false, dir, tupdatum, tupnull,
+									   array, cur, &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum satisfies both low_compare and high_compare, so
+				 * it's time to advance the array keys.
+				 *
+				 * Note: It's possible that the skip array will "advance" from
+				 * its MINVAL (or MAXVAL) representation to an alternative,
+				 * logically equivalent representation of the same value: a
+				 * representation where the = key gets a valid datum in its
+				 * sk_argument.  This is only possible when low_compare uses
+				 * the >= strategy (or high_compare uses the <= strategy).
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1017,18 +1598,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1053,18 +1625,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -1080,14 +1643,22 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			bool		cur_elem_trig = (sktrig_required && ikey == sktrig);
 
 			/*
-			 * Binary search for closest match that's available from the array
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems == -1)
+				_bt_binsrch_skiparray_skey(cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * Binary search for the closest match from the SAOP array
+			 */
+			else
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 		}
 		else
 		{
@@ -1163,11 +1734,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+		if (array)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->num_elems == -1)
+			{
+				/* Skip array's "binary search" determines its new element */
+				_bt_skiparray_set_element(rel, cur, array, result,
+										  tupdatum, tupnull);
+			}
+			else if (array->cur_elem != set_elem)
+			{
+				/* SAOP array has new set_elem (returned by binary search) */
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
 		}
 	}
 
@@ -1580,10 +2161,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -1914,6 +2496,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key uses one of several sentinel values.  We just
+		 * fall back on _bt_tuple_before_array_skeys when we see such a value.
+		 */
+		if (key->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL |
+							 SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index dd6f5a15c..817ad358f 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -106,6 +106,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 2c325badf..303622f9d 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("btree equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (amoid == HASH_AM_OID)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 8a7fb6a22..4e7a67b16 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -143,6 +143,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index e199f0716..1d3d901dc 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -371,6 +371,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index f279853de..4227ab1a7 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f23cfad71..244f48f4f 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index c2918c9c8..7315a288f 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -94,7 +94,6 @@
 
 #include "postgres.h"
 
-#include <ctype.h>
 #include <math.h>
 
 #include "access/brin.h"
@@ -193,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -214,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5768,6 +5771,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6826,6 +6915,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6835,17 +6971,21 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
+	double		correlation = 0;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
+	bool		set_correlation = false;
 	bool		eqQualHere;
-	bool		found_saop;
+	bool		found_array;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	bool		upper_inequal_col;
+	bool		lower_inequal_col;
 	double		num_sa_scans;
+	double		inequalselectivity;
 	ListCell   *lc;
 
 	/*
@@ -6861,16 +7001,21 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
-	 * operator can be considered to act the same as it normally does.
+	 * If there's a ScalarArrayOpExpr in the quals, or if we expect to
+	 * generate a skip scan array, then we'll actually perform up to N index
+	 * descents (not just one), but the underlying operator can be considered
+	 * to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
-	found_saop = false;
+	found_array = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
+	upper_inequal_col = false;
+	lower_inequal_col = false;
 	num_sa_scans = 1;
+	inequalselectivity = 1;
 	foreach(lc, path->indexclauses)
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
@@ -6878,13 +7023,103 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		past_first_skipped = false;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
-			eqQualHere = false;
-			indexcol++;
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't come after a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT,
+							new_num_sa_scans;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						Assert(!set_correlation);
+						correlation = btcost_correlation(index, &vardata);
+						set_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (!past_first_skipped)
+				{
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is...
+					 */
+					if (!upper_inequal_col)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lower_inequal_col)
+						ndistinct += 1;
+
+					past_first_skipped = true;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				found_array = true;
+				new_num_sa_scans = num_sa_scans * ndistinct;
+
+				/*
+				 * Stop adding new skip arrays when the would-be new
+				 * num_sa_scans exceeds a third of the number of index pages
+				 */
+				if (ceil(index->pages * 0.3333333) < new_num_sa_scans)
+					break;
+
+				/* Done costing skipping for this index column */
+				num_sa_scans = new_num_sa_scans;
+				indexcol++;
+			}
+
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+				break;			/* no quals for indexcol (can't skip scan) */
+
+			/* reset for next indexcol */
+			eqQualHere = false;
+			upper_inequal_col = false;
+			lower_inequal_col = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6906,6 +7141,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6914,7 +7150,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				double		alength = estimate_array_length(root, other_operand);
 
 				clause_op = saop->opno;
-				found_saop = true;
+				found_array = true;
 				/* estimate SA descents by indexBoundQuals only */
 				if (alength > 1)
 					num_sa_scans *= alength;
@@ -6926,7 +7162,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skip scan purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6942,6 +7178,34 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct.
+					 *
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (!upper_inequal_col)
+							inequalselectivity =
+								Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+									DEFAULT_RANGE_INEQ_SEL);
+						upper_inequal_col = true;
+					}
+					else
+					{
+						if (!lower_inequal_col)
+							inequalselectivity =
+								Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+									DEFAULT_RANGE_INEQ_SEL);
+						lower_inequal_col = true;
+					}
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6957,7 +7221,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
-		!found_saop &&
+		!found_array &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
 	else
@@ -6997,14 +7261,18 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * of leaf pages (we make it 1/3 the total number of pages instead) to
 		 * give the btree code credit for its ability to continue on the leaf
 		 * level with low selectivity scans.
+		 *
+		 * Note: num_sa_scans is derived in a way that treats any skip scan
+		 * quals as if they were ScalarArrayOpExpr quals whose array has as
+		 * many distinct elements as possibly-matching values in the index.
 		 */
 		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
 		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
-		 * to integer.
+		 * ScalarArrayOpExpr quals (as well as any skip scan quals) included
+		 * in indexBoundQuals, and then round to integer.
 		 *
 		 * It is tempting to make genericcostestimate behave as if SAOP
 		 * clauses work in almost the same way as scalar operators during
@@ -7059,110 +7327,25 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * cost is somewhat arbitrarily set at 50x cpu_operator_cost per page
 	 * touched.  The number of such pages is btree tree height plus one (ie,
 	 * we charge for the leaf page too).  As above, charge once per estimated
-	 * SA index descent.
+	 * index descent.
 	 */
 	descentCost = (index->tree_height + 1) * DEFAULT_PAGE_CPU_MULTIPLIER * cpu_operator_cost;
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!set_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..2bd35d2d2
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns skip support struct, allocating in caller's memory
+ * context.  Otherwise returns NULL, indicating that operator class has no
+ * skip support function.
+ */
+SkipSupport
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse)
+{
+	Oid			skipSupportFunction;
+	SkipSupport sksup;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return NULL;
+
+	sksup = palloc(sizeof(SkipSupportData));
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return sksup;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 9682f9dbd..347089b76 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2304,6 +2305,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 4f8402ef9..1b72059d2 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,6 +13,7 @@
 
 #include "postgres.h"
 
+#include <limits.h>
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
@@ -21,6 +22,7 @@
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -416,6 +418,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index d17fcbd5c..1f1aafb36 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -829,7 +829,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 11cdfac0d..0bf3e39f3 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4283,7 +4283,10 @@ description | Waiting for a newly initialized WAL file to reach durable storage
      <replaceable>value1</replaceable> OR
      <replaceable>expression</replaceable> = <replaceable>value2</replaceable>
      ...</literal> constructs when the query planner transforms the constructs
-    into an equivalent multi-valued array representation.
+    into an equivalent multi-valued array representation.  Similarly, when
+    B-Tree index scans use the skip scan strategy, an index search is
+    performed each time the scan is repositioned to the next index leaf page
+    that might have matching tuples.
    </para>
   </note>
   <tip>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 053619624..7e23a7b6e 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254..f5aa73ac7 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  btree equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 8879554c3..bfb1a286e 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -581,6 +581,47 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Index Only Scan using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Index Only Scan Backward using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 --
 -- Test for multilevel page deletion
 --
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index bd5f002cf..15dde752f 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1637,7 +1637,9 @@ DROP TABLE syscol_table;
 -- Tests for IS NULL/IS NOT NULL with b-tree indexes
 --
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 SET enable_seqscan = OFF;
 SET enable_indexscan = ON;
@@ -1645,7 +1647,7 @@ SET enable_bitmapscan = ON;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1657,13 +1659,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1678,12 +1680,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1695,13 +1703,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1722,12 +1730,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0,
      1
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc nulls last,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1739,13 +1753,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1760,12 +1774,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2  nulls first,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1777,13 +1797,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1798,6 +1818,12 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 -- Check initial-positioning logic too
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2);
@@ -1829,20 +1855,24 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
 (2 rows)
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-         |        
-     278 |     999
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 5;
+ unique1 |  unique2   
+---------+------------
+     500 |           
+     100 |           
+         |           
+         | 2147483647
+     278 |        999
+(5 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-     278 |     999
-       0 |     998
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 3;
+ unique1 |  unique2   
+---------+------------
+         | 2147483647
+     278 |        999
+       0 |        998
+(3 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
@@ -2247,7 +2277,8 @@ SELECT count(*) FROM dupindexcols
 (1 row)
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 explain (costs off)
 SELECT unique1 FROM tenk1
@@ -2269,7 +2300,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2289,7 +2320,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2309,6 +2340,25 @@ ORDER BY thousand DESC, tenthous DESC;
         0 |     3000
 (2 rows)
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ Index Only Scan Backward using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > 995) AND (tenthous = ANY ('{998,999}'::integer[])))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+ thousand | tenthous 
+----------+----------
+      999 |      999
+      998 |      998
+(2 rows)
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -2339,6 +2389,45 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 ---------
 (0 rows)
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+                   QUERY PLAN                    
+-------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY (NULL::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY ('{NULL,NULL,NULL}'::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: ((unique1 IS NULL) AND (unique1 IS NULL))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+ unique1 
+---------
+(0 rows)
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
                                 QUERY PLAN                                 
@@ -2462,6 +2551,44 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 ---------
 (0 rows)
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > '-1'::integer) AND (thousand >= 0) AND (tenthous = 3000))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+(1 row)
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand < 3) AND (thousand <= 2) AND (tenthous = 1001))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        1 |     1001
+(1 row)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 6543e90de..6ebd6265f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5331,9 +5331,10 @@ Function              | in_range(time without time zone,time without time zone,i
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql
index 670ad5c6e..68c61dbc7 100644
--- a/src/test/regress/sql/btree_index.sql
+++ b/src/test/regress/sql/btree_index.sql
@@ -327,6 +327,27 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 
 --
 -- Test for multilevel page deletion
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index be570da08..40ba3d65b 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -650,7 +650,9 @@ DROP TABLE syscol_table;
 --
 
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 
 SET enable_seqscan = OFF;
@@ -663,6 +665,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -675,6 +678,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NUL
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0, 1);
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -686,6 +690,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -697,6 +702,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -716,9 +722,9 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
   ORDER BY unique2 LIMIT 2;
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 5;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 3;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
 
@@ -852,7 +858,8 @@ SELECT count(*) FROM dupindexcols
   WHERE f1 BETWEEN 'WA' AND 'ZZZ' and id < 1000 and f1 ~<~ 'YX';
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 
 explain (costs off)
@@ -864,7 +871,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -874,7 +881,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -884,6 +891,15 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand DESC, tenthous DESC;
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -897,6 +913,21 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 
 SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('{33, 44}'::bigint[]);
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
 
@@ -942,6 +973,26 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
 SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b09d8af71..1ab169b14 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -220,6 +220,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2697,6 +2698,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.47.2

v25-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/x-patch; name=v25-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From b06dc9591ab60cd4716bd8fc6de3925e44e184a3 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v25 1/6] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.  It's also likely to make behavior that will be introduced by
an upcoming patch to add skip scan optimizations easier to understand.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  15 +++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  40 +++++++
 contrib/bloom/blscan.c                        |   1 +
 doc/src/sgml/bloom.sgml                       |   7 +-
 doc/src/sgml/monitoring.sgml                  |  27 +++--
 doc/src/sgml/perform.sgml                     |   8 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 22 files changed, 254 insertions(+), 49 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index dc6e01842..0d1ad8379 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -147,6 +147,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 60320440f..9f147ce4e 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -589,6 +589,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index 7d1e66152..81440a283 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -436,6 +436,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index cc40e928e..609e85fda 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index a3a1fccf3..c4f730437 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 07bae342e..369e39bc4 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 411d5ac0b..fe8578eed 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -70,6 +70,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -555,6 +556,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -581,6 +583,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -708,6 +711,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			Assert(btscan->btps_nextScanPage != P_NONE);
 			*next_scan_page = btscan->btps_nextScanPage;
@@ -808,6 +816,12 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+
+	/*
+	 * Don't use local primscan counter -- overwrite it with the authoritative
+	 * primscan counter, which we maintain in shared memory
+	 */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -842,6 +856,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 472ce06f1..941b4eaaf 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -950,6 +950,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 53f910e9d..8554b4535 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c0d614866..9f64e1357 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2135,6 +2137,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2148,6 +2151,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2164,6 +2168,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2682,6 +2687,41 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+
+	if (!es->analyze)
+		return;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	ExplainPropertyFloat("Index Searches", NULL, nsearches, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index bf801fe78..472169d61 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -116,6 +116,7 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++)
 	{
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 6a8a60b8c..4cddfaae1 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -174,9 +174,10 @@ CREATE INDEX
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..178436.00 rows=1 width=0) (actual time=20.005..20.005 rows=2300 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
          Buffers: shared hit=19608
+         Index Searches: 1
  Planning Time: 0.099 ms
  Execution Time: 22.632 ms
-(10 rows)
+(11 rows)
 </programlisting>
   </para>
 
@@ -209,12 +210,14 @@ CREATE INDEX
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..4.52 rows=11 width=0) (actual time=0.026..0.026 rows=7 loops=1)
                Index Cond: (i5 = 123451)
                Buffers: shared hit=3
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..4.52 rows=11 width=0) (actual time=0.007..0.007 rows=8 loops=1)
                Index Cond: (i2 = 898732)
                Buffers: shared hit=3
+               Index Searches: 1
  Planning Time: 0.264 ms
  Execution Time: 0.047 ms
-(13 rows)
+(15 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index e698e74e1..11cdfac0d 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4269,16 +4269,31 @@ description | Waiting for a newly initialized WAL file to reach durable storage
 
   <note>
    <para>
-    Queries that use certain <acronym>SQL</acronym> constructs to search for
-    rows matching any value out of a list or array of multiple scalar values
-    (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    Index scans may sometimes perform multiple index searches per execution.
+    Each index search increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    This can happen with queries that use certain <acronym>SQL</acronym>
+    constructs to search for rows matching any value out of a list or array of
+    multiple scalar values (see <xref linkend="functions-comparisons"/>).  It
+    can also happen with queries that have
+    <literal><replaceable>expression</replaceable> =
+     <replaceable>value1</replaceable> OR
+     <replaceable>expression</replaceable> = <replaceable>value2</replaceable>
+     ...</literal> constructs when the query planner transforms the constructs
+    into an equivalent multi-valued array representation.
+   </para>
   </note>
+  <tip>
+   <para>
+    <command>EXPLAIN ANALYZE</command> outputs the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
+  </tip>
 
  </sect2>
 
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aab..14fd8b337 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -730,9 +730,11 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 &lt; 10)
                Buffers: shared hit=2
+               Index Searches: 1
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
          Buffers: shared hit=24 read=6
+         Index Searches: 10
  Planning:
    Buffers: shared hit=15 dirtied=9
  Planning Time: 0.485 ms
@@ -791,6 +793,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 &lt; 100)
                            Buffers: shared hit=2
+                           Index Searches: 1
  Planning:
    Buffers: shared hit=12
  Planning Time: 0.187 ms
@@ -860,6 +863,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
 -------------------------------------------------------------------&zwsp;-------------------------------------------------------
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
+   Index Searches: 1
    Rows Removed by Index Recheck: 1
    Buffers: shared hit=1
  Planning Time: 0.039 ms
@@ -894,8 +898,10 @@ EXPLAIN (ANALYZE, BUFFERS OFF) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND un
    -&gt;  BitmapAnd  (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0 loops=1)
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
  Planning Time: 0.162 ms
  Execution Time: 0.143 ms
 </screen>
@@ -924,6 +930,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
                Index Cond: (unique1 &lt; 100)
                Buffers: shared read=2
+               Index Searches: 1
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
 
@@ -1059,6 +1066,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
    Buffers: shared hit=16
    -&gt;  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
          Index Cond: (unique2 &gt; 9000)
+         Index Searches: 1
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
          Buffers: shared hit=16
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index 652ece721..9e91bcda0 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -507,9 +507,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
          Buffers: shared hit=4
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(9 rows)
+(10 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 9fdf8b1d9..80a63b5f1 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index f2d146581..a5df4107a 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index dbd01066d..925e698d4 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1.00 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1.00 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2.00 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4.00 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1.00 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2.00 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1.00 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 -------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2.00 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 ----------------------------------------------------------------------------------
  Nested Loop (actual rows=4 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2.00 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2.00 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4.00 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4.00 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32 loops=N)
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4.00 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4.00 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 -------------------------------------------------------------------------------------
  Nested Loop (actual rows=16 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4.00 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 1dbe6ff54..2671478f9 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2369,6 +2369,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2686,47 +2690,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2742,16 +2755,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2770,7 +2786,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2786,16 +2802,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2816,7 +2835,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2887,16 +2906,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
@@ -2904,17 +2926,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2990,17 +3015,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3.00 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2.00 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -3011,17 +3042,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1.00 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1.00 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3056,17 +3093,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=4.60 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2.00 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 5
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2.75 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 4
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1.00 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3077,17 +3120,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0.60 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1.00 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0.33 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 3
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3141,17 +3190,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3173,17 +3228,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3511,12 +3572,14 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3532,9 +3595,10 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3582,13 +3646,17 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
              ->  Limit (actual rows=1 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4158,14 +4226,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 88911ca2b..cde2eac73 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -----------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index d5aab4e56..2197b0ac5 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 6aad02156..fe2959e91 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -588,6 +588,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.47.2

In reply to: Peter Geoghegan (#65)
6 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Sun, Feb 23, 2025 at 12:19 PM Peter Geoghegan <pg@bowt.ie> wrote:

The patch series recently bitrot due to conflicting changes on HEAD,
so I decided to post a new v25 now.

Attached is v26, which has no functional changes. This is just to fix
yet more bitrot.

--
Peter Geoghegan

Attachments:

v26-0005-Apply-low-order-skip-key-in-_bt_first-more-often.patchapplication/octet-stream; name=v26-0005-Apply-low-order-skip-key-in-_bt_first-more-often.patchDownload
From 687a2e244e0eee6760197269ce5292d9f5cf3226 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 13 Jan 2025 16:08:32 -0500
Subject: [PATCH v26 5/6] Apply low-order skip key in _bt_first more often.

Convert low_compare and high_compare nbtree skip array inequalities
(with opclasses that offer skip support) such that _bt_first is
consistently able to use later keys when descending the tree within
_bt_first.

For example, an index qual "WHERE a > 5 AND b = 2" is now converted to
"WHERE a >= 6 AND b = 2" by a new preprocessing step that takes place
after a final low_compare and/or high_compare are chosen by all earlier
preprocessing steps.  That way the scan's initial call to _bt_first will
use "WHERE a >= 6 AND b = 2" to find the initial leaf level position,
rather than merely using "WHERE a > 5" -- "b = 2" can always be applied.
This has a decent chance of making the scan avoid an extra _bt_first
call that would otherwise be needed just to determine the lowest-sorting
"a" value in the index (the lowest that still satisfies "WHERE a > 5").

The transformation process can only lower the total number of index
pages read when the use of a more restrictive set of initial positioning
keys in _bt_first actually allows the scan to land on some later leaf
page directly, relative to the unoptimized case (or on an earlier leaf
page directly, when scanning backwards).  The savings can be far greater
when affected skip arrays come after some higher-order array.  For
example, a qual "WHERE x IN (1, 2, 3) AND y > 5 AND z = 2" can now save
as many as 3 _bt_first calls as a result of these transformations (there
can be as many as 1 _bt_first call saved per "x" array element).

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=FJ78K3WsF3iWNxWnUCY9f=Jdg3QPxaXE=uYUbmuRz5Q@mail.gmail.com
---
 src/backend/access/nbtree/nbtpreprocesskeys.c | 175 ++++++++++++++++++
 src/test/regress/expected/create_index.out    |  21 +++
 src/test/regress/sql/create_index.sql         |  10 +
 3 files changed, 206 insertions(+)

diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 3d8ff701f..3f47f58f1 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -50,6 +50,12 @@ static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
 								 BTArrayKeyInfo *array, bool *qual_ok);
 static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
 								 BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+									   BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
@@ -1288,6 +1294,166 @@ _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
 	return true;
 }
 
+/*
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts their
+ * operator strategies, while changing the operator as appropriate).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value MINVAL, using a transformed >= key
+ * instead of using the original > key makes it safe to include lower-order
+ * scan keys in the insertion scan key (there must be lower-order scan keys
+ * after the skip array).  We will avoid an extra _bt_first to find the first
+ * value in the index > sk_argument -- at least when the first real matching
+ * value in the index happens to be an exact match for the sk_argument value
+ * that we produced here by incrementing the original input key's sk_argument.
+ * (Backwards scans derive the same benefit when they encounter the sentinel
+ * value MAXVAL, by converting the high_compare key from < to <=.)
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01'.
+ */
+static void
+_bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+						   BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	/*
+	 * Called last among all preprocessing steps, when the skip array's final
+	 * low_compare and high_compare have both been chosen
+	 */
+	Assert(so->skipScan);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1 && !array->null_elem && array->sksup);
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skiparray_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skiparray_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1824,6 +1990,15 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && array->sksup &&
+						!array->null_elem)
+						_bt_skiparray_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 15dde752f..fa4517e2d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2589,6 +2589,27 @@ ORDER BY thousand;
         1 |     1001
 (1 row)
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+                                            QUERY PLAN                                            
+--------------------------------------------------------------------------------------------------
+ Limit
+   ->  Index Only Scan using tenk1_thous_tenthous on tenk1
+         Index Cond: ((thousand > '-1'::integer) AND (tenthous = ANY ('{1001,3000}'::integer[])))
+(3 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+        1 |     1001
+(2 rows)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index 40ba3d65b..e8c16959a 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -993,6 +993,16 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
 ORDER BY thousand;
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
-- 
2.47.2

v26-0004-Lower-nbtree-skip-array-maintenance-overhead.patchapplication/octet-stream; name=v26-0004-Lower-nbtree-skip-array-maintenance-overhead.patchDownload
From 3d463490a84e50bd866f05fa971df023d513d5bb Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v26 4/6] Lower nbtree skip array maintenance overhead.

Add an optimization that fixes regressions in index scans that are
nominally eligible to use skip scan, but can never actually benefit from
skipping.  These are cases where a leading prefix column contains many
distinct values -- especially when the number of values approaches the
total number of index tuples, where skipping can never be profitable.

The optimization is activated dynamically, as a fallback strategy.  It
works by determining a prefix of leading index columns whose scan keys
(often skip array scan keys) are guaranteed to be satisfied by every
possible index tuple on a given page.  _bt_readpage is then able to
start comparisons at the first scan key that might not be satisfied.
This necessitates making _bt_readpage temporarily cease maintaining the
scan's arrays.  _bt_checkkeys will treat the scan's keys as if they were
not marked as required during preprocessing.  This process relies on the
non-required SAOP array logic in _bt_advance_array_keys that was added
to Postgres 17 by commit 5bf748b8.

The new optimization does not affect array primitive scan scheduling.
It is similar to the precheck optimization added by Postgres 17 commit
e0b1ee17dc, though it is only used during nbtree scans with skip arrays.
It can be applied during scans that were never eligible for the precheck
optimization.  As a result, many scans that cannot benefit from skipping
will still benefit from using skip arrays (skip arrays indirectly enable
the use of the optimization introduced by this commit).

Skip scan's approach of adding skip arrays during preprocessing and then
fixing (or significantly ameliorating) the resulting regressions seen in
unsympathetic cases is enabled by the optimization added by this commit
(and by the "look ahead" optimization introduced by commit 5bf748b8).
This allows the planner to avoid generating distinct, competing index
paths (one path for skip scan, another for an equivalent traditional
full index scan).  The overall effect is to make scan runtime close to
optimal, even when the planner works off an incorrect cardinality
estimate.  Scans will also perform well given a skipped column with data
skew: individual groups of pages with many distinct values in respect of
a skipped column can be read about as efficiently as before, without
having to give up on skipping over other provably-irrelevant leaf pages.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h           |   5 +-
 src/backend/access/nbtree/nbtsearch.c |  48 ++++
 src/backend/access/nbtree/nbtutils.c  | 395 +++++++++++++++++++++++---
 3 files changed, 401 insertions(+), 47 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 10d9f8186..ef20e9932 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1115,11 +1115,13 @@ typedef struct BTReadPageState
 
 	/*
 	 * Input and output parameters, set and unset by both _bt_readpage and
-	 * _bt_checkkeys to manage precheck optimizations
+	 * _bt_checkkeys to manage precheck and forcenonrequired optimizations
 	 */
 	bool		firstpage;		/* on first page of current primitive scan? */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
+	bool		forcenonrequired;	/* treat all scan keys as nonrequired? */
+	int			ikey;			/* start comparisons from ikey'th scan key */
 
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
@@ -1328,6 +1330,7 @@ extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arra
 						  IndexTuple tuple, int tupnatts);
 extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
 									 IndexTuple finaltup);
+extern void _bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 3e06a260e..45ed6e489 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1650,6 +1650,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.firstpage = firstpage;
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
+	pstate.forcenonrequired = false;
+	pstate.ikey = 0;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
@@ -1732,6 +1734,14 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 													   so->currPos.currPage);
 					return false;
 				}
+
+				/*
+				 * Consider temporarily disabling array maintenance during
+				 * skip scan primitive index scans that have read more than
+				 * one leaf page (unless we've reached the rightmost page)
+				 */
+				if (!pstate.firstpage && so->skipScan && minoff < maxoff)
+					_bt_skip_ikeyprefix(scan, &pstate);
 			}
 
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
@@ -1773,6 +1783,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum < pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1836,6 +1847,15 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			IndexTuple	itup = (IndexTuple) PageGetItem(page, iid);
 			int			truncatt;
 
+			if (pstate.forcenonrequired)
+			{
+				Assert(so->skipScan);
+
+				/* recover from treating the scan's keys as nonrequired */
+				_bt_start_array_keys(scan, dir);
+				pstate.forcenonrequired = false;
+				pstate.ikey = 0;
+			}
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
@@ -1872,6 +1892,14 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 													   so->currPos.currPage);
 					return false;
 				}
+
+				/*
+				 * Consider temporarily disabling array maintenance during
+				 * skip scan primitive index scans that have read more than
+				 * one leaf page (unless we've reached the leftmost page)
+				 */
+				if (!pstate.firstpage && so->skipScan && minoff < maxoff)
+					_bt_skip_ikeyprefix(scan, &pstate);
 			}
 
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
@@ -1916,6 +1944,15 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff && pstate.forcenonrequired)
+			{
+				Assert(so->skipScan);
+
+				/* recover from treating the scan's keys as nonrequired */
+				_bt_start_array_keys(scan, dir);
+				pstate.forcenonrequired = false;
+				pstate.ikey = 0;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
@@ -1927,6 +1964,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum > pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1992,6 +2030,16 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 		so->currPos.itemIndex = MaxTIDsPerBTreePage - 1;
 	}
 
+	/*
+	 * As far as our caller is concerned, the scan's arrays always track its
+	 * progress through the index's key space.
+	 *
+	 * If _bt_skip_ikeyprefix told us to temporarily treat all scan keys as
+	 * nonrequired (during a skip scan), then we must recover afterwards by
+	 * advancing our arrays using finaltup (with !pstate.forcenonrequired).
+	 */
+	Assert(!pstate.forcenonrequired);
+
 	return (so->currPos.firstItem <= so->currPos.lastItem);
 }
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index f61a2bade..464bc50f5 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -57,11 +57,12 @@ static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool forcenonrequired,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-								 ScanDirection dir, bool *continuescan);
+								 ScanDirection dir, bool forcenonrequired, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -1415,14 +1416,14 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
  * postcondition's <= operator with a >=.  In other words, just swap the
  * precondition with the postcondition.)
  *
- * We also deal with "advancing" non-required arrays here.  Callers whose
- * sktrig scan key is non-required specify sktrig_required=false.  These calls
- * are the only exception to the general rule about always advancing the
+ * We also deal with "advancing" non-required arrays here (or arrays that are
+ * treated as non-required for the duration of a _bt_readpage call).  Callers
+ * whose sktrig scan key is non-required specify sktrig_required=false.  These
+ * calls are the only exception to the general rule about always advancing the
  * required array keys (the scan may not even have a required array).  These
  * callers should just pass a NULL pstate (since there is never any question
  * of stopping the scan).  No call to _bt_tuple_before_array_skeys is required
- * ahead of these calls (it's already clear that any required scan keys must
- * be satisfied by caller's tuple).
+ * ahead of these calls.
  *
  * Note that we deal with non-array required equality strategy scan keys as
  * degenerate single element arrays here.  Obviously, they can never really
@@ -1474,8 +1475,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		pstate->firstmatch = false;
 
-		/* Shouldn't have to invalidate 'prechecked', though */
-		Assert(!pstate->prechecked);
+		/* Shouldn't have to invalidate precheck/forcenonrequired state */
+		Assert(!pstate->prechecked && !pstate->forcenonrequired &&
+			   pstate->ikey == 0);
 
 		/*
 		 * Once we return we'll have a new set of required array keys, so
@@ -1484,6 +1486,26 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		pstate->rechecks = 0;
 		pstate->targetdistance = 0;
 	}
+	else if (sktrig < so->numberOfKeys - 1 &&
+			 !(so->keyData[so->numberOfKeys - 1].sk_flags & SK_SEARCHARRAY))
+	{
+		int			least_sign_ikey = so->numberOfKeys - 1;
+		bool		continuescan;
+
+		/*
+		 * Optimization: perform a precheck of the least significant key
+		 * during !sktrig_required calls when it isn't already our sktrig
+		 * (provided the precheck key is not itself an array).
+		 *
+		 * When the precheck works out we'll avoid an expensive binary search
+		 * of sktrig's array (plus any other arrays before least_sign_ikey).
+		 */
+		Assert(so->keyData[sktrig].sk_flags & SK_SEARCHARRAY);
+		if (!_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
+							   false, false, false, false,
+							   &continuescan, &least_sign_ikey))
+			return false;
+	}
 
 	Assert(_bt_verify_keys_with_arraykeys(scan));
 
@@ -1527,8 +1549,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -1662,7 +1682,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -1704,7 +1724,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -1719,7 +1739,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -1779,6 +1799,12 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * of any required scan key).  All that matters is whether caller's tuple
 	 * satisfies the new qual, so it's safe to just skip the _bt_check_compare
 	 * recheck when we've already determined that it can only return 'false'.
+	 *
+	 * Note: In practice most scan keys are marked required by preprocessing,
+	 * if necessary by generating a preceding skip array.  We nevertheless
+	 * often handle array keys marked required as if they were nonrequired.
+	 * This behavior is requested by our _bt_check_compare caller, though only
+	 * when it is passed "forcenonrequired=true" by _bt_checkkeys.
 	 */
 	if ((sktrig_required && all_required_satisfied) ||
 		(!sktrig_required && all_satisfied))
@@ -1790,7 +1816,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -2034,20 +2060,23 @@ new_prim_scan:
 	 * the scan arrives on the next sibling leaf page) when it has already
 	 * read at least one leaf page before the one we're reading now.  This is
 	 * important when reading subsets of an index with many distinct values in
-	 * respect of an attribute constrained by an array.  It encourages fewer,
-	 * larger primitive scans where that makes sense.
-	 *
-	 * Note: so->scanBehind is primarily used to indicate that the scan
-	 * encountered a finaltup that "satisfied" one or more required scan keys
-	 * on a truncated attribute value/-inf value.  We can safely reuse it to
-	 * force the scan to stay on the leaf level because the considerations are
-	 * exactly the same.
+	 * respect of an attribute constrained by an array (often a skip array).
+	 * It encourages fewer, larger primitive scans where that makes sense.
+	 * This will in turn encourage _bt_readpage to apply the forcenonrequired
+	 * optimization when applicable (i.e. when the scan has a skip array).
 	 *
 	 * Note: This heuristic isn't as aggressive as you might think.  We're
 	 * conservative about allowing a primitive scan to step from the first
 	 * leaf page it reads to the page's sibling page (we only allow it on
 	 * first pages whose finaltup strongly suggests that it'll work out).
 	 * Clearing this first page finaltup hurdle is a strong signal in itself.
+	 *
+	 * Note: so->scanBehind is primarily used to indicate that the scan
+	 * encountered a finaltup that "satisfied" one or more required scan keys
+	 * on a truncated attribute value/-inf value.  We reuse it to force the
+	 * scan to stay on the leaf level because the considerations are just the
+	 * same (the array's are ahead of the index key space, or they're behind
+	 * when we're scanning backwards).
 	 */
 	if (!pstate->firstpage)
 	{
@@ -2223,14 +2252,16 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	TupleDesc	tupdesc = RelationGetDescr(scan->indexRelation);
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ScanDirection dir = so->currPos.dir;
-	int			ikey = 0;
+	int			ikey = pstate->ikey;
 	bool		res;
 
+	Assert(ikey == 0 || pstate->forcenonrequired);
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+							arrayKeys, pstate->forcenonrequired,
+							pstate->prechecked, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
@@ -2241,12 +2272,12 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 *
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
-		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
+		Assert(!pstate->prechecked && !pstate->firstmatch &&
+			   !pstate->forcenonrequired);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
-	if (pstate->prechecked || pstate->firstmatch)
+	if ((pstate->prechecked || pstate->firstmatch) && !pstate->forcenonrequired)
 	{
 		bool		dcontinuescan;
 		int			dikey = 0;
@@ -2256,7 +2287,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, false, false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -2279,6 +2310,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * It's also possible that the scan is still _before_ the _start_ of
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
+	Assert(!pstate->forcenonrequired);
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
@@ -2394,7 +2426,7 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	Assert(so->numArrayKeys);
 
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -2402,6 +2434,231 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	return true;
 }
 
+/*
+ * Determine a prefix of scan keys that are guaranteed to be satisfied by
+ * every possible tuple on pstate's page.  Used during scans with skip arrays,
+ * which do not use the similar optimization controlled by pstate.prechecked.
+ *
+ * Sets pstate.ikey (and pstate.forcenonrequired) on success, making later
+ * calls to _bt_checkkeys start checks of each tuple from the so->keyData[]
+ * entry at pstate.ikey (while treating keys >= pstate.ikey as nonrequired).
+ *
+ * When _bt_checkkeys treats the scan's required keys as non-required, the
+ * scan's array keys won't be properly maintained (they won't have advanced in
+ * lockstep with our progress through the index's key space as expected).
+ * Caller must recover from this by restarting the scan's array keys and
+ * resetting pstate.ikey and pstate.forcenonrequired just ahead of the
+ * _bt_checkkeys call for the page's final tuple (the pstate.finaltup tuple).
+ */
+void
+_bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	ScanDirection dir = so->currPos.dir;
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	ItemId		iid;
+	IndexTuple	firsttup,
+				lasttup;
+	int			ikey = 0,
+				arrayidx = 0,
+				firstchangingattnum;
+
+	Assert(so->skipScan && pstate->minoff < pstate->maxoff);
+
+	/* minoff is an offset to the lowest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->minoff);
+	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* maxoff is an offset to the highest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->maxoff);
+	lasttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* Determine the first attribute whose values change on caller's page */
+	firstchangingattnum = _bt_keep_natts_fast(rel, firsttup, lasttup);
+
+	for (; ikey < so->numberOfKeys; ikey++)
+	{
+		ScanKey		key = so->keyData + ikey;
+		BTArrayKeyInfo *array;
+		Datum		tupdatum;
+		bool		tupnull;
+		int32		result;
+
+		/*
+		 * Determine if it's safe to set pstate.ikey to an offset to a key
+		 * that comes after this key, by examining this key
+		 */
+		if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) == 0)
+		{
+			/*
+			 * This is the first key that is not marked required (only happens
+			 * when _bt_preprocess_keys couldn't mark all keys required due to
+			 * implementation restrictions affecting skip array generation)
+			 */
+			Assert(!(key->sk_flags & SK_BT_SKIP));
+			break;				/* pstate.ikey to be set to nonrequired ikey */
+		}
+		if (key->sk_strategy != BTEqualStrategyNumber)
+		{
+			/*
+			 * This is the scan's first inequality key (barring inequalities
+			 * that are used to describe the range of a = skip array key).
+			 *
+			 * We could handle this like a = key, but it doesn't seem worth
+			 * the trouble.  Have _bt_checkkeys start with this inequality.
+			 */
+			break;				/* pstate.ikey to be set to inequality's ikey */
+		}
+		if (!(key->sk_flags & SK_SEARCHARRAY))
+		{
+			/*
+			 * Found a scalar (non-array) = key.
+			 *
+			 * It is unsafe to set pstate.ikey to an ikey beyond this key,
+			 * unless the = key is satisfied by every possible tuple on the
+			 * page (possible only when attribute has just one distinct value
+			 * among all tuples on the page).
+			 */
+			if (key->sk_attno < firstchangingattnum)
+			{
+				/*
+				 * Only one distinct value on the page for this key's column.
+				 * Must make sure that = key is actually satisfied by the
+				 * value that is stored within every tuple on the page.
+				 */
+				tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+										 &tupnull);
+				result = _bt_compare_array_skey(&so->orderProcs[ikey],
+												tupdatum, tupnull,
+												key->sk_argument, key);
+				if (result == 0)
+					continue;	/* safe, = key satisfied by every tuple */
+			}
+			break;				/* pstate.ikey to be set to scalar key's ikey */
+		}
+
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == ikey);
+		if (array->num_elems != -1)
+		{
+			/*
+			 * Found a SAOP array = key.
+			 *
+			 * Handle this just like we handle scalar = keys.
+			 */
+			if (key->sk_attno < firstchangingattnum)
+			{
+				/*
+				 * Only one distinct value on the page for this key's column.
+				 * Must make sure that SAOP array is actually satisfied by the
+				 * value that is stored within every tuple on the page.
+				 */
+				tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+										 &tupnull);
+				_bt_binsrch_array_skey(&so->orderProcs[ikey], false,
+									   NoMovementScanDirection,
+									   tupdatum, tupnull, array, key, &result);
+				if (result == 0)
+					continue;	/* safe, SAOP = key satisfied by every tuple */
+			}
+			break;				/* pstate.ikey to be set to SAOP array's ikey */
+		}
+
+		/*
+		 * Found a skip array = key.
+		 *
+		 * As with other = keys, moving past this skip array key is safe when
+		 * every tuple on the page is guaranteed to satisfy the array's key.
+		 * But we need a slightly different approach, since skip arrays make
+		 * it easy to assess whether all the values on the page fall within
+		 * the skip array's entire range.
+		 */
+		if (array->null_elem)
+		{
+			/* Safe, non-range skip array "satisfied" by every tuple on page */
+			continue;
+		}
+		else if (key->sk_attno > firstchangingattnum)
+		{
+			/*
+			 * We cannot assess whether this range skip array will definitely
+			 * be satisfied by every tuple on the page, since its attribute is
+			 * preceded by another attribute that is not certain to contain
+			 * the same prefix of value(s) within every tuple from pstate.page
+			 */
+			break;				/* pstate.ikey to be set to range array's ikey */
+		}
+
+		/*
+		 * Found a range skip array = key.
+		 *
+		 * It's definitely safe for _bt_checkkeys to avoid assessing this
+		 * range skip array when the page's first and last non-pivot tuples
+		 * both satisfy the range skip array (since the same must also be true
+		 * of all the tuples in between these two).
+		 */
+		tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   tupdatum, tupnull, array, key, &result);
+		if (result != 0)
+			break;				/* pstate.ikey to be set to range array's ikey */
+
+		tupdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   tupdatum, tupnull, array, key, &result);
+		if (result != 0)
+			break;				/* pstate.ikey to be set to range array's ikey */
+
+		/* Safe, range skip array satisfied by every tuple on page */
+	}
+
+	/*
+	 * If pstate.ikey remains 0, _bt_advance_array_keys will still be able to
+	 * apply its precheck optimization when dealing with "nonrequired" array
+	 * keys.  That's reason enough on its own to set forcenonrequired=true.
+	 */
+	pstate->forcenonrequired = true;	/* do this unconditionally */
+	pstate->ikey = ikey;
+
+	/*
+	 * Set the element for range skip arrays whose ikey is >= pstate.ikey to
+	 * whatever the first array element is in the scan's current direction.
+	 * This allows range skip arrays that will never be satisfied by any tuple
+	 * on the page to avoid extra sk_argument comparisons -- _bt_check_compare
+	 * won't use the key's sk_argument when the key is marked MINVAL/MAXVAL
+	 * (note that MINVAL/MAXVAL won't be unset until an exact match is found,
+	 * which might not happen for any tuple on the page).
+	 *
+	 * Set the element for non-range skip arrays whose ikey is >= pstate.ikey
+	 * to NULL (regardless of whether NULLs are stored first or last), too.
+	 * This allows non-range skip arrays (recognized by _bt_check_compare as
+	 * "non-required" skip arrays with ISNULL set) to avoid needlessly calling
+	 * _bt_advance_array_keys (we know that any non-range skip array must be
+	 * satisfied by every possible indexable value, so this is always safe).
+	 */
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		key = &so->keyData[array->scan_key];
+
+		Assert(key->sk_flags & SK_SEARCHARRAY);
+
+		if (array->num_elems != -1)
+			continue;
+		if (array->scan_key < pstate->ikey)
+			continue;
+
+		Assert(key->sk_flags & SK_BT_SKIP);
+
+		if (!array->null_elem)
+			_bt_array_set_low_or_high(rel, key, array,
+									  ScanDirectionIsForward(dir));
+		else
+			_bt_skiparray_set_isnull(rel, key, array);
+	}
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
@@ -2433,17 +2690,25 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Though we advance non-required array keys on our own, that shouldn't have
  * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
+ * have no fixed relationship with the scan's progress.
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass forcenonrequired=true to instruct us to treat all keys as nonrequried.
+ * This is used to make it safe to temporarily stop properly maintaining the
+ * scan's required arrays.  Callers can determine which prefix of keys must
+ * satisfy every possible prefix of index attribute values on the page, and
+ * then pass us an initial *ikey for the first key that might be unsatisified.
+ * We won't be maintaining any arrays before that initial *ikey, so there is
+ * no point in trying to do so for any later arrays.  (Callers that do this
+ * must be careful to reset the array keys when they finish reading the page.)
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool forcenonrequired,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -2460,10 +2725,13 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when we're forced to treat all scan keys as nonrequired)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (forcenonrequired)
+			Assert(!prechecked);
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
@@ -2511,6 +2779,19 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		{
 			Assert(key->sk_flags & SK_SEARCHARRAY);
 			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir || forcenonrequired);
+
+			/*
+			 * Cannot fall back on _bt_tuple_before_array_skeys when we're
+			 * treating the scan's keys as nonrequired, though.  Just handle
+			 * this like any other non-required equality-type array key.
+			 */
+			if (forcenonrequired)
+			{
+				Assert(!(key->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+				return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
+											  tupdesc, *ikey, false);
+			}
 
 			*continuescan = false;
 			return false;
@@ -2520,7 +2801,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
-									 continuescan))
+									 forcenonrequired, continuescan))
 				continue;
 			return false;
 		}
@@ -2552,9 +2833,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			 */
 			if (requiredSameDir)
 				*continuescan = false;
+			else if (unlikely(key->sk_flags & SK_BT_SKIP))
+			{
+				/*
+				 * If we're treating scan keys as nonrequired, and encounter a
+				 * skip array scan key whose current element is NULL, then it
+				 * must be a non-range skip array.  It must be satisfied, so
+				 * there's no need to call _bt_advance_array_keys to check.
+				 */
+				Assert(forcenonrequired && *ikey > 0);
+				continue;
+			}
 
 			/*
-			 * In any case, this indextuple doesn't match the qual.
+			 * This indextuple doesn't match the qual.
 			 */
 			return false;
 		}
@@ -2575,7 +2867,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -2593,7 +2885,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -2662,7 +2954,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
  */
 static bool
 _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
-					 TupleDesc tupdesc, ScanDirection dir, bool *continuescan)
+					 TupleDesc tupdesc, ScanDirection dir,
+					 bool forcenonrequired, bool *continuescan)
 {
 	ScanKey		subkey = (ScanKey) DatumGetPointer(skey->sk_argument);
 	int32		cmpresult = 0;
@@ -2702,7 +2995,11 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 
 		if (isNull)
 		{
-			if (subkey->sk_flags & SK_BT_NULLS_FIRST)
+			if (forcenonrequired)
+			{
+				/* treating scan key as non-required */
+			}
+			else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -2756,8 +3053,12 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			 */
 			Assert(subkey != (ScanKey) DatumGetPointer(skey->sk_argument));
 			subkey--;
-			if ((subkey->sk_flags & SK_BT_REQFWD) &&
-				ScanDirectionIsForward(dir))
+			if (forcenonrequired)
+			{
+				/* treating scan key as non-required */
+			}
+			else if ((subkey->sk_flags & SK_BT_REQFWD) &&
+					 ScanDirectionIsForward(dir))
 				*continuescan = false;
 			else if ((subkey->sk_flags & SK_BT_REQBKWD) &&
 					 ScanDirectionIsBackward(dir))
@@ -2809,7 +3110,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			break;
 	}
 
-	if (!result)
+	if (!result && !forcenonrequired)
 	{
 		/*
 		 * Tuple fails this qual.  If it's a required qual for the current
@@ -2853,6 +3154,8 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	Assert(!pstate->forcenonrequired);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
-- 
2.47.2

v26-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchapplication/octet-stream; name=v26-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patchDownload
From e3ccac132272a41d8303b488254f6cb55a200f79 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 14 Aug 2024 13:50:23 -0400
Subject: [PATCH v26 1/6] Show index search count in EXPLAIN ANALYZE.

Expose the information tracked by pg_stat_*_indexes.idx_scan to EXPLAIN
ANALYZE output.  This is particularly useful for index scans that use
ScalarArrayOp quals, where the number of index scans isn't predictable
in advance with optimizations like the ones added to nbtree by commit
5bf748b8.  It's also likely to make behavior that will be introduced by
an upcoming patch to add skip scan optimizations easier to understand.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/relscan.h                  |   3 +
 src/backend/access/brin/brin.c                |   1 +
 src/backend/access/gin/ginscan.c              |   1 +
 src/backend/access/gist/gistget.c             |   2 +
 src/backend/access/hash/hashsearch.c          |   1 +
 src/backend/access/index/genam.c              |   1 +
 src/backend/access/nbtree/nbtree.c            |  15 +++
 src/backend/access/nbtree/nbtsearch.c         |   1 +
 src/backend/access/spgist/spgscan.c           |   1 +
 src/backend/commands/explain.c                |  40 +++++++
 contrib/bloom/blscan.c                        |   1 +
 doc/src/sgml/bloom.sgml                       |   6 +-
 doc/src/sgml/monitoring.sgml                  |  27 +++--
 doc/src/sgml/perform.sgml                     |   7 ++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   1 +
 src/test/regress/expected/brin_multi.out      |  27 +++--
 src/test/regress/expected/memoize.out         |  50 ++++++---
 src/test/regress/expected/partition_prune.out | 100 +++++++++++++++---
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   6 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 22 files changed, 252 insertions(+), 49 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index dc6e01842..0d1ad8379 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -147,6 +147,9 @@ typedef struct IndexScanDescData
 	bool		xactStartedInRecovery;	/* prevents killing/seeing killed
 										 * tuples */
 
+	/* index access method instrumentation output state */
+	uint64		nsearches;		/* # of index searches */
+
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 75a65ec9c..9f146c12a 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -591,6 +591,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	scan->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index 63ded6301..8c1bbf366 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -437,6 +437,7 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index cc40e928e..609e85fda 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		scan->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +751,7 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index a3a1fccf3..c4f730437 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,7 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 07bae342e..369e39bc4 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -118,6 +118,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xactStartedInRecovery = TransactionStartedDuringRecovery();
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
+	scan->nsearches = 0;		/* deliberately not reset by index_rescan */
 	scan->opaque = NULL;
 
 	scan->xs_itup = NULL;
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 45ea6afba..ca2a6e8f8 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -70,6 +70,7 @@ typedef struct BTParallelScanDescData
 	BTPS_State	btps_pageStatus;	/* indicates whether next page is
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
+	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
 	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
@@ -557,6 +558,7 @@ btinitparallelscan(void *target)
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	bt_target->btps_nsearches = 0;
 	ConditionVariableInit(&bt_target->btps_cv);
 }
 
@@ -583,6 +585,7 @@ btparallelrescan(IndexScanDesc scan)
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
+	/* deliberately don't reset btps_nsearches (matches index_rescan) */
 	SpinLockRelease(&btscan->btps_mutex);
 }
 
@@ -710,6 +713,11 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			 * We have successfully seized control of the scan for the purpose
 			 * of advancing it to a new page!
 			 */
+			if (first && btscan->btps_pageStatus == BTPARALLEL_NOT_INITIALIZED)
+			{
+				/* count the first primitive scan for this btrescan */
+				btscan->btps_nsearches++;
+			}
 			btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
 			Assert(btscan->btps_nextScanPage != P_NONE);
 			*next_scan_page = btscan->btps_nextScanPage;
@@ -810,6 +818,12 @@ _bt_parallel_done(IndexScanDesc scan)
 		btscan->btps_pageStatus = BTPARALLEL_DONE;
 		status_changed = true;
 	}
+
+	/*
+	 * Don't use local primscan counter -- overwrite it with the authoritative
+	 * primscan counter, which we maintain in shared memory
+	 */
+	scan->nsearches = btscan->btps_nsearches;
 	SpinLockRelease(&btscan->btps_mutex);
 
 	/* wake up all the workers associated with this parallel scan */
@@ -844,6 +858,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nextScanPage = InvalidBlockNumber;
 		btscan->btps_lastCurrPage = InvalidBlockNumber;
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
+		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
 		for (int i = 0; i < so->numArrayKeys; i++)
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 472ce06f1..941b4eaaf 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -950,6 +950,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	scan->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 53f910e9d..8554b4535 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,7 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 4271dd48e..15ca901ca 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/relscan.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -88,6 +89,7 @@ static void show_plan_tlist(PlanState *planstate, List *ancestors,
 static void show_expression(Node *node, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							bool useprefix, ExplainState *es);
+static void show_indexscan_nsearches(PlanState *planstate, ExplainState *es);
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
@@ -2113,6 +2115,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexScan:
 			show_scan_qual(((IndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2126,6 +2129,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
 										   planstate, es);
@@ -2142,6 +2146,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexscan_nsearches(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2660,6 +2665,41 @@ show_expression(Node *node, const char *qlabel,
 	ExplainPropertyText(qlabel, exprstr, es);
 }
 
+/*
+ * Show the number of index searches within an IndexScan node, IndexOnlyScan
+ * node, or BitmapIndexScan node
+ */
+static void
+show_indexscan_nsearches(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	struct IndexScanDescData *scanDesc = NULL;
+	uint64		nsearches = 0;
+
+	if (!es->analyze)
+		return;
+
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			scanDesc = ((IndexScanState *) planstate)->iss_ScanDesc;
+			break;
+		case T_IndexOnlyScan:
+			scanDesc = ((IndexOnlyScanState *) planstate)->ioss_ScanDesc;
+			break;
+		case T_BitmapIndexScan:
+			scanDesc = ((BitmapIndexScanState *) planstate)->biss_ScanDesc;
+			break;
+		default:
+			break;
+	}
+
+	if (scanDesc)
+		nsearches = scanDesc->nsearches;
+
+	ExplainPropertyFloat("Index Searches", NULL, nsearches, 0, es);
+}
+
 /*
  * Show a qualifier expression (which is a List with implicit AND semantics)
  */
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index bf801fe78..472169d61 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -116,6 +116,7 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
+	scan->nsearches++;
 
 	for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++)
 	{
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 663a0a4a6..9d9cf6df9 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -173,10 +173,11 @@ CREATE INDEX
    Buffers: shared hit=21864
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..178436.00 rows=1 width=0) (actual time=20.005..20.005 rows=2300.00 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
          Buffers: shared hit=19608
  Planning Time: 0.099 ms
  Execution Time: 22.632 ms
-(10 rows)
+(11 rows)
 </programlisting>
   </para>
 
@@ -211,10 +212,11 @@ CREATE INDEX
                Buffers: shared hit=3
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..4.52 rows=11 width=0) (actual time=0.007..0.007 rows=8.00 loops=1)
                Index Cond: (i2 = 898732)
+               Index Searches: 1
                Buffers: shared hit=3
  Planning Time: 0.264 ms
  Execution Time: 0.047 ms
-(13 rows)
+(15 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 9178f1d34..55e3b382f 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4228,16 +4228,31 @@ description | Waiting for a newly initialized WAL file to reach durable storage
 
   <note>
    <para>
-    Queries that use certain <acronym>SQL</acronym> constructs to search for
-    rows matching any value out of a list or array of multiple scalar values
-    (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    Index scans may sometimes perform multiple index searches per execution.
+    Each index search increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    This can happen with queries that use certain <acronym>SQL</acronym>
+    constructs to search for rows matching any value out of a list or array of
+    multiple scalar values (see <xref linkend="functions-comparisons"/>).  It
+    can also happen with queries that have
+    <literal><replaceable>expression</replaceable> =
+     <replaceable>value1</replaceable> OR
+     <replaceable>expression</replaceable> = <replaceable>value2</replaceable>
+     ...</literal> constructs when the query planner transforms the constructs
+    into an equivalent multi-valued array representation.
+   </para>
   </note>
+  <tip>
+   <para>
+    <command>EXPLAIN ANALYZE</command> outputs the total number of index
+    searches performed by each index scan node.  <literal>Index Searches: N</literal>
+    indicates the total number of searches across <emphasis>all</emphasis>
+    executor node executions/loops.
+   </para>
+  </tip>
 
  </sect2>
 
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index be4b49f62..9c51f5869 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -729,9 +729,11 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Buffers: shared hit=3 read=5 written=4
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10.00 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
                Buffers: shared hit=2
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 10
          Buffers: shared hit=24 read=6
  Planning:
    Buffers: shared hit=15 dirtied=9
@@ -790,6 +792,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Buffers: shared hit=92
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100.00 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
                            Buffers: shared hit=2
  Planning:
    Buffers: shared hit=12
@@ -861,6 +864,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0.00 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
    Rows Removed by Index Recheck: 1
+   Index Searches: 1
    Buffers: shared hit=1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -896,6 +900,7 @@ EXPLAIN (ANALYZE, BUFFERS OFF) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND un
                Index Cond: (unique1 &lt; 100)
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999.00 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
  Planning Time: 0.162 ms
  Execution Time: 0.143 ms
 </screen>
@@ -923,6 +928,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Buffers: shared hit=4 read=2
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100.00 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared read=2
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
@@ -1061,6 +1067,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
          Index Cond: (unique2 &gt; 9000)
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
+         Index Searches: 1
          Buffers: shared hit=16
  Planning Time: 0.077 ms
  Execution Time: 0.086 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index 7daddf03e..e2507bb9d 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -507,9 +507,10 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99.00 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
          Buffers: shared hit=4
+         Index Searches: 1
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(9 rows)
+(10 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 1d9924a2a..7ddab09a5 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1045,6 +1045,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
  Aggregate  (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1.00 loops=1)
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0.00 loops=1)
          Index Cond: (word = 'caterpiler'::text)
+         Index Searches: 1
          Heap Fetches: 0
  Planning time: 0.164 ms
  Execution time: 0.117 ms
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index 991b7eaca..cb5b5e53e 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 22f2d3284..e83ad3b5d 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -48,8 +50,9 @@ WHERE t2.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1.00 loops=N)
                      Index Cond: (unique1 = t2.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -79,8 +82,9 @@ WHERE t1.unique1 < 1000;', false);
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1.00 loops=N)
                      Index Cond: (unique1 = t1.twenty)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +110,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20.00 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10.00 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2.00 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +120,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4.00 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -146,10 +152,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1.00 loops=N)
+                     Index Searches: N
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -217,9 +224,10 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
          Hits: 20  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2.00 loops=N)
                Index Cond: (x = (t1.t)::numeric)
+               Index Searches: N
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -245,8 +253,9 @@ WHERE t2.unique1 < 1200;', true);
                Hits: N  Misses: N  Evictions: N  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1.00 loops=N)
                      Index Cond: (unique1 = t2.thousand)
+                     Index Searches: N
                      Heap Fetches: N
-(12 rows)
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -260,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
 ----------------------------------------------------------------------------------
  Nested Loop (actual rows=4.00 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2.00 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2.00 loops=N)
          Cache Key: f1.f
@@ -267,8 +277,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          Hits: 1  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2.00 loops=N)
                Index Cond: (f = f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -277,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
 ----------------------------------------------------------------------------------
  Nested Loop (actual rows=4.00 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2.00 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=2.00 loops=N)
          Cache Key: f1.f
@@ -284,8 +296,9 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          Hits: 0  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2.00 loops=N)
                Index Cond: (f <= f1.f)
+               Index Searches: N
                Heap Fetches: N
-(10 rows)
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +324,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4.00 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +341,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4.00 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -347,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
  Append (actual rows=32.00 loops=N)
    ->  Nested Loop (actual rows=16.00 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4.00 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4.00 loops=N)
                Cache Key: t1_1.a
@@ -354,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4.00 loops=N)
                      Index Cond: (a = t1_1.a)
+                     Index Searches: N
                      Heap Fetches: N
    ->  Nested Loop (actual rows=16.00 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4.00 loops=N)
+               Index Searches: N
                Heap Fetches: N
          ->  Memoize (actual rows=4.00 loops=N)
                Cache Key: t1_2.a
@@ -364,8 +382,9 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                Hits: 3  Misses: 1  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4.00 loops=N)
                      Index Cond: (a = t1_2.a)
+                     Index Searches: N
                      Heap Fetches: N
-(21 rows)
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -377,6 +396,7 @@ ON t1.a = t2.a;', false);
 ----------------------------------------------------------------------------------------
  Nested Loop (actual rows=16.00 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4.00 loops=N)
+         Index Searches: N
          Heap Fetches: N
    ->  Memoize (actual rows=4.00 loops=N)
          Cache Key: t1.a
@@ -385,11 +405,13 @@ ON t1.a = t2.a;', false);
          ->  Append (actual rows=4.00 loops=N)
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4.00 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0.00 loops=N)
                      Index Cond: (a = t1.a)
+                     Index Searches: N
                      Heap Fetches: N
-(14 rows)
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index d95d2395d..1bd64ca6a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2369,6 +2369,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2686,47 +2690,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0.00 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0.00 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2742,16 +2755,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2770,7 +2786,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2786,16 +2802,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0.00 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
@@ -2816,7 +2835,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2887,16 +2906,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1.00 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1.00 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0.00 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1.00 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1.00 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1.00 loops=1)
@@ -2904,17 +2926,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1.00 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1.00 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0.00 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1.00 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2990,17 +3015,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3.00 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2.00 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2.00 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -3011,17 +3042,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1.00 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1.00 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3056,17 +3093,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=4.60 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2.00 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 5
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2.75 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 4
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1.00 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3077,17 +3120,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0.60 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1.00 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0.33 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 3
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3141,17 +3190,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1.00 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1.00 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3173,17 +3228,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0.00 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3511,12 +3572,14 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1.00 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1.00 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3532,9 +3595,10 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Sort Key: ma_test.b
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1.00 loops=1)
+         Index Searches: 1
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3582,13 +3646,17 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
              ->  Limit (actual rows=1.00 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1.00 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
+         Index Searches: 0
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10.00 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10.00 loops=1)
+         Index Searches: 1
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4158,14 +4226,18 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
    ->  Merge Append (actual rows=0.00 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0.00 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0.00 loops=1)
+               Index Searches: 1
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
+               Index Searches: 0
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0.00 loops=1)
+         Index Searches: 1
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index cd79abc35..aa55b6a82 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -763,8 +763,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 --------------------------------------------------------------------
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1.00 loops=1)
    Index Cond: (unique2 = 11)
+   Index Searches: 1
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index d5aab4e56..2197b0ac5 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,10 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: 0', 'Index Searches: Zero');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 5f36d589b..4a2c74b08 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -588,6 +588,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
-- 
2.47.2

v26-0006-DEBUG-Add-skip-scan-disable-GUCs.patchapplication/octet-stream; name=v26-0006-DEBUG-Add-skip-scan-disable-GUCs.patchDownload
From e1ee98c879f6a09c2df1484106f218ab51a638a2 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 18 Jan 2025 10:54:44 -0500
Subject: [PATCH v26 6/6] DEBUG: Add skip scan disable GUCs.

---
 src/include/access/nbtree.h                   |  4 +++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 31 +++++++++++++++++++
 src/backend/utils/misc/guc_tables.c           | 23 ++++++++++++++
 3 files changed, 58 insertions(+)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index ef20e9932..565d13fe6 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1184,6 +1184,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 3f47f58f1..b2dfb3eed 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -21,6 +21,27 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 typedef struct BTScanKeyPreproc
 {
 	ScanKey		inkey;
@@ -1636,6 +1657,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
 			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
 
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].sksup = NULL;
+
 			/*
 			 * We'll need a 3-way ORDER proc to perform "binary searches" for
 			 * the next matching array element.  Set that up now.
@@ -2141,6 +2166,12 @@ _bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
 		if (attno_has_rowcompare)
 			break;
 
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
 		/*
 		 * Now consider next attno_inkey (or keep going if this is an
 		 * additional scan key against the same attribute)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index ad25cbb39..7e4fa5c31 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1784,6 +1785,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3661,6 +3673,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
-- 
2.47.2

v26-0003-Add-nbtree-skip-scan-optimizations.patchapplication/octet-stream; name=v26-0003-Add-nbtree-skip-scan-optimizations.patchDownload
From f1d0cb124c85c657a0cdffe87ba91ef5dc1bb1f0 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v26 3/6] Add nbtree skip scan optimizations.

Teach nbtree composite index scans to opportunistically skip over
irrelevant sections of composite indexes given a query with an omitted
prefix column.  When nbtree is passed input scan keys derived from a
query predicate "WHERE b = 5", new nbtree preprocessing steps now output
"WHERE a = ANY(<every possible 'a' value>) AND b = 5" scan keys.  That
is, preprocessing generates a "skip array" (along with an associated
scan key) for the omitted column "a", which makes it safe to mark the
scan key on "b" as required to continue the scan.  This is far more
efficient than a traditional full index scan whenever it allows the scan
to skip over many irrelevant leaf pages, by iteratively repositioning
itself using the keys on "a" and "b" together.

A skip array has "elements" that are generated procedurally and on
demand, but otherwise work just like conventional ScalarArrayOp arrays.
Preprocessing can freely add a skip array before or after any input
ScalarArrayOp arrays.  Index scans with a skip array decide when and
where to reposition the scan using the same approach as any other scan
with array keys.  This design builds on the design for array advancement
and primitive scan scheduling added to Postgres 17 by commit 5bf748b8.

The core B-Tree operator classes on most discrete types generate their
array elements with the help of their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the next
possible indexable value frequently turns out to be the next value
stored in the index.  Opclasses that lack a skip support routine fall
back on having nbtree "increment" (or "decrement") a skip array's
current element by setting the NEXT (or PRIOR) scan key flag, without
directly changing the scan key's sk_argument.  These sentinel values
behave just like any other value from an array -- though they can never
locate equal index tuples (they can only locate the next group of index
tuples containing the next set of non-sentinel values that the scan's
arrays need to advance to).

Inequality scan keys can affect how skip arrays generate their values.
Their range is constrained by the inequalities.  For example, a skip
array on "a" will only use element values 1 and 2 given a qual such as
"WHERE a BETWEEN 1 AND 2 AND b = 66".  A scan using such a skip array
has almost identical performance characteristics to one with the qual
"WHERE a = ANY('{1, 2}') AND b = 66".  The scan will be much faster when
executed as two selective primitive index scans, rather than as a single
very large index scan that reads many irrelevant index leaf pages.
However, the array transformation process won't always lead to improved
performance at runtime.  Much depends on physical index characteristics.

B-Tree preprocessing is optimistic about skipping working out: it
applies static, generic rules when determining where to generate skip
arrays, which assumes that the runtime overhead of maintaining skip
arrays will pay for itself -- or lead to only a modest performance loss.
As things stand, these assumptions are much too optimistic: skip array
maintenance will lead to unacceptable regressions with unsympathetic
queries (typically scans with little to no potential to avoid reading
irrelevant leaf pages).  An upcoming commit will address the problems in
this area by adding a mechanism that greatly lowers the cost of array
maintenance in unfavorable cases.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |   3 +-
 src/include/access/nbtree.h                   |  35 +-
 src/include/catalog/pg_amproc.dat             |  22 +
 src/include/catalog/pg_proc.dat               |  27 +
 src/include/storage/lwlock.h                  |   1 +
 src/include/utils/skipsupport.h               |  98 +++
 src/backend/access/index/indexam.c            |   3 +-
 src/backend/access/nbtree/nbtcompare.c        | 273 +++++++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 601 ++++++++++++--
 src/backend/access/nbtree/nbtree.c            | 211 ++++-
 src/backend/access/nbtree/nbtsearch.c         | 111 ++-
 src/backend/access/nbtree/nbtutils.c          | 748 ++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |   4 +
 src/backend/commands/opclasscmds.c            |  25 +
 src/backend/storage/lmgr/lwlock.c             |   1 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/date.c                  |  46 ++
 src/backend/utils/adt/meson.build             |   1 +
 src/backend/utils/adt/selfuncs.c              | 430 +++++++---
 src/backend/utils/adt/skipsupport.c           |  61 ++
 src/backend/utils/adt/timestamp.c             |  48 ++
 src/backend/utils/adt/uuid.c                  |  70 ++
 doc/src/sgml/btree.sgml                       |  34 +-
 doc/src/sgml/indexam.sgml                     |   3 +-
 doc/src/sgml/indices.sgml                     |  49 +-
 doc/src/sgml/monitoring.sgml                  |   5 +-
 doc/src/sgml/xindex.sgml                      |  16 +-
 src/test/regress/expected/alter_generic.out   |  10 +-
 src/test/regress/expected/btree_index.out     |  41 +
 src/test/regress/expected/create_index.out    | 183 ++++-
 src/test/regress/expected/psql.out            |   3 +-
 src/test/regress/sql/alter_generic.sql        |   5 +-
 src/test/regress/sql/btree_index.sql          |  21 +
 src/test/regress/sql/create_index.sql         |  63 +-
 src/tools/pgindent/typedefs.list              |   3 +
 36 files changed, 2880 insertions(+), 377 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index bf729a1e4..cb5c5eab4 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -214,7 +214,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 947431954..10d9f8186 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -707,6 +708,10 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
  *	(BTOPTIONS_PROC).  These procedures define a set of user-visible
  *	parameters that can be used to control operator class behavior.  None of
  *	the built-in B-Tree operator classes currently register an "options" proc.
+ *
+ *	To facilitate more efficient B-Tree skip scans, an operator class may
+ *	choose to offer a sixth amproc procedure (BTSKIPSUPPORT_PROC).  For full
+ *	details, see src/include/utils/skipsupport.h.
  */
 
 #define BTORDER_PROC		1
@@ -714,7 +719,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1027,10 +1033,21 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
-	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+	int			cur_elem;		/* index of current element in elem_values */
+
+	/* fields for skip arrays, which generate element datums procedurally */
+	int16		attlen;			/* attr len in bytes */
+	bool		attbyval;		/* as FormData_pg_attribute.attbyval */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupport sksup;			/* skip support (NULL if opclass lacks it) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1042,6 +1059,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* arrays might be ahead of scan's key space? */
 	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
@@ -1119,6 +1137,15 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on column without input = */
+
+/* SK_BT_SKIP-only flags (mutable, set and unset by array advancement) */
+#define SK_BT_MINVAL	0x00080000	/* invalid sk_argument, use low_compare */
+#define SK_BT_MAXVAL	0x00100000	/* invalid sk_argument, use high_compare */
+#define SK_BT_NEXT		0x00200000	/* positions the scan > sk_argument */
+#define SK_BT_PRIOR		0x00400000	/* positions the scan < sk_argument */
+
+/* Remaps pg_index flag bits to uppermost SK_BT_* byte */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1165,7 +1192,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 19100482b..37000639f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cd9422d0b..75698ff09 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2266,6 +2281,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4456,6 +4474,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6328,6 +6349,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9376,6 +9400,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 13a7dc899..ffa03189e 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -194,6 +194,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_LOCK_MANAGER,
 	LWTRANCHE_PREDICATE_LOCK_MANAGER,
 	LWTRANCHE_PARALLEL_HASH_JOIN,
+	LWTRANCHE_PARALLEL_BTREE_SCAN,
 	LWTRANCHE_PARALLEL_QUERY_DSA,
 	LWTRANCHE_PER_SESSION_DSA,
 	LWTRANCHE_PER_SESSION_RECORD_TYPE,
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..bc51847cf
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining groups of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * It usually isn't feasible to implement skip support for an opclass whose
+ * input type is continuous.  The B-Tree code falls back on next-key sentinel
+ * values for any opclass that doesn't provide its own skip support function.
+ * This isn't really an implementation restriction; there is no benefit to
+ * providing skip support for an opclass where guessing that the next indexed
+ * value is the next possible indexable value never (or hardly ever) works out.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order)
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 * Operator class decrement/increment functions will never be called with
+	 * a NULL "existing" argument, either.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern SkipSupport PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+												 bool reverse);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 8b1f55543..0e62d7a7a 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -470,7 +470,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 291cb8fc1..4da5a3c1d 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 1fd1da5f1..3d8ff701f 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -45,8 +45,15 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
+static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
+								 ScanKey skey, FmgrInfo *orderproc,
+								 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
+								 BTArrayKeyInfo *array, bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
+							   int *numSkipArrayKeys);
 static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
 									  Oid elemtype, StrategyNumber strat,
 									  Datum *elems, int nelems);
@@ -89,6 +96,8 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -101,10 +110,16 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never generated skip array scan keys, it would be possible for "gaps"
+ * to appear that make it unsafe to mark any subsequent input scan keys
+ * (copied from scan->keyData[]) as required to continue the scan.  Prior to
+ * Postgres 18, a qual like "WHERE y = 4" always required a full index scan.
+ * It'll now become "WHERE x = ANY('{every possible x value}') and y = 4" on
+ * output, due to the addition of a skip array on "x".  This has the potential
+ * to be much more efficient than a full index scan (though it'll behave like
+ * a full index scan when there are many distinct values for "x").
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -141,7 +156,12 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -200,6 +220,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProcs[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -231,6 +259,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
+		Assert(!so->skipScan);
 
 		return;
 	}
@@ -288,7 +317,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -345,7 +375,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -393,6 +422,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, _bt_preprocess_array_keys has already added "=" keys
+			 * sufficient to form an unbroken series of "=" constraints on all
+			 * attrs prior to the attr from the final scan->keyData[] key.
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -481,6 +515,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -489,6 +524,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -508,8 +544,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -803,6 +837,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -812,6 +849,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -885,6 +938,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -978,24 +1032,55 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
  * Compare an array scan key to a scalar scan key, eliminating contradictory
  * array elements such that the scalar scan key becomes redundant.
  *
+ * If the opfamily is incomplete we may not be able to determine which
+ * elements are contradictory.  When we return true we'll have validly set
+ * *qual_ok, guaranteeing that at least the scalar scan key can be considered
+ * redundant.  We return false if the comparison could not be made (caller
+ * must keep both scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar scan keys.
+ */
+static bool
+_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
+							   bool *qual_ok)
+{
+	Assert(arraysk->sk_attno == skey->sk_attno);
+	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
+		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
+	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
+		   skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Just call the appropriate helper function based on whether it's a SAOP
+	 * array or a skip array.  Both helpers will set *qual_ok in passing.
+	 */
+	if (array->num_elems != -1)
+		return _bt_saoparray_shrink(scan, arraysk, skey, orderproc, array,
+									qual_ok);
+	else
+		return _bt_skiparray_shrink(scan, skey, array, qual_ok);
+}
+
+/*
+ * Preprocessing of SAOP (non-skip) array scan key, used to determine which
+ * array elements are eliminated as contradictory by a non-array scalar key.
+ * _bt_compare_array_scankey_args helper function.
+ *
  * Array elements can be eliminated as contradictory when excluded by some
  * other operator on the same attribute.  For example, with an index scan qual
  * "WHERE a IN (1, 2, 3) AND a < 2", all array elements except the value "1"
  * are eliminated, and the < scan key is eliminated as redundant.  Cases where
  * every array element is eliminated by a redundant scalar scan key have an
  * unsatisfiable qual, which we handle by setting *qual_ok=false for caller.
- *
- * If the opfamily doesn't supply a complete set of cross-type ORDER procs we
- * may not be able to determine which elements are contradictory.  If we have
- * the required ORDER proc then we return true (and validly set *qual_ok),
- * guaranteeing that at least the scalar scan key can be considered redundant.
- * We return false if the comparison could not be made (caller must keep both
- * scan keys when this happens).
  */
 static bool
-_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
-							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
-							   bool *qual_ok)
+_bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+					 FmgrInfo *orderproc, BTArrayKeyInfo *array, bool *qual_ok)
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
@@ -1006,14 +1091,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
-	Assert(arraysk->sk_attno == skey->sk_attno);
 	Assert(array->num_elems > 0);
-	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
-		   arraysk->sk_strategy == BTEqualStrategyNumber);
-	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
-		   skey->sk_strategy != BTEqualStrategyNumber);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
 
 	/*
 	 * _bt_binsrch_array_skey searches an array for the entry best matching a
@@ -1112,6 +1191,103 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	return true;
 }
 
+/*
+ * Preprocessing of skip (non-SAOP) array scan key, used to determine
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function.
+ *
+ * Unlike _bt_saoparray_shrink, we don't modify caller's array in-place.  Skip
+ * arrays work by procedurally generating their elements as needed, so we just
+ * store the inequality as the skip array's low_compare or high_compare.  The
+ * array's elements will be generated from the range of values that satisfies
+ * both low_compare and high_compare.
+ */
+static bool
+_bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
+					 bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Array's index attribute will be constrained by a strict operator/key.
+	 * Array must not "contain a NULL element" (i.e. the scan must not apply
+	 * "IS NULL" qual when it reaches the end of the index that stores NULLs).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Consider if we should treat caller's scalar scan key as the skip
+	 * array's high_compare or low_compare.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way the scan can always safely assume that
+	 * it's okay to use the input-opclass-only-type proc from so->orderProcs[]
+	 * (they can be cross-type with SAOP arrays, but never with skip arrays).
+	 *
+	 * This approach is enabled by MINVAL/MAXVAL sentinel key markings, which
+	 * can be thought of as representing either the lowest or highest matching
+	 * array element (excluding the NULL element, where applicable, though as
+	 * just discussed it isn't applicable to this range skip array anyway).
+	 * Array keys marked MINVAL/MAXVAL never has a valid datum stored in its
+	 * sk_argument.  The scan must directly apply the array's low_compare when
+	 * it encounters MINVAL (or its high_compare when it encounters MAXVAL),
+	 * and must never use the array's so->orderProcs[] proc against
+	 * low_compare's/high_compare's sk_argument.
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* replace existing high_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing high_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's high_compare */
+			array->high_compare = skey;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* replace existing low_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing low_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's low_compare */
+			array->low_compare = skey;
+			break;
+		case BTEqualStrategyNumber:
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1137,6 +1313,12 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -1152,41 +1334,36 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	Oid			skip_eq_ops[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
-	ScanKey		cur;
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
+	so->skipScan = (numSkipArrayKeys > 0);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys we'll need to generate below
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -1201,18 +1378,20 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
+		ScanKey		inkey = scan->keyData + input_ikey,
+					cur;
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
 		Oid			elemtype;
@@ -1225,21 +1404,114 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		Datum	   *elem_values;
 		bool	   *elem_nulls;
 		int			num_nonnulls;
-		int			j;
+
+		/* set up next output scan key */
+		cur = &arrayKeyData[numArrayKeyData];
+
+		/* Backfill skip arrays for attrs < or <= input key's attr? */
+		while (numSkipArrayKeys && attno_skip <= inkey->sk_attno)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skip_eq_ops[attno_skip - 1];
+			CompactAttribute *attr;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/*
+				 * Attribute already has an = input key, so don't output a
+				 * skip array for attno_skip.  Just copy attribute's = input
+				 * key into arrayKeyData[] once outside this inner loop.
+				 *
+				 * Note: When we get here there must be a later attribute that
+				 * lacks an equality input key, and still needs a skip array
+				 * (if there wasn't then numSkipArrayKeys would be 0 by now).
+				 */
+				Assert(attno_skip == inkey->sk_attno);
+				/* inkey can't be last input key to be marked required: */
+				Assert(input_ikey < scan->numberOfKeys - 1);
+#if 0
+				/* Could be a redundant input scan key, so can't do this: */
+				Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+					   (inkey->sk_flags & SK_SEARCHNULL));
+#endif
+
+				attno_skip++;
+				break;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize generic BTArrayKeyInfo fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+
+			/* Initialize skip array specific BTArrayKeyInfo fields */
+			attr = TupleDescCompactAttr(RelationGetDescr(rel), attno_skip - 1);
+			reverse = (indoption[attno_skip - 1] & INDOPTION_DESC) != 0;
+			so->arrayKeys[numArrayKeys].attlen = attr->attlen;
+			so->arrayKeys[numArrayKeys].attbyval = attr->attbyval;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup =
+				PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse);
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * We'll need a 3-way ORDER proc to perform "binary searches" for
+			 * the next matching array element.  Set that up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			numArrayKeys++;
+			numArrayKeyData++;	/* keep this scan key/array */
+
+			/* set up next output scan key */
+			cur = &arrayKeyData[numArrayKeyData];
+
+			/* remember having output this skip array and scan key */
+			numSkipArrayKeys--;
+			attno_skip++;
+		}
 
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
-		*cur = scan->keyData[input_ikey];
+		*cur = *inkey;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -1257,7 +1529,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * all btree operators are strict.
 		 */
 		num_nonnulls = 0;
-		for (j = 0; j < num_elems; j++)
+		for (int j = 0; j < num_elems; j++)
 		{
 			if (!elem_nulls[j])
 				elem_values[num_nonnulls++] = elem_values[j];
@@ -1295,7 +1567,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -1306,7 +1578,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -1323,7 +1595,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -1392,23 +1664,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			origelemtype = elemtype;
 		}
 
-		/*
-		 * And set up the BTArrayKeyInfo data.
-		 *
-		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
-		 * scan_key field later on, after so->keyData[] has been finalized.
-		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		/* Initialize generic BTArrayKeyInfo fields */
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
+
+		/* Initialize SAOP array specific BTArrayKeyInfo fields */
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].cur_elem = -1;	/* i.e. invalid */
+
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
+	Assert(numSkipArrayKeys == 0);
+
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -1514,7 +1787,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -1565,7 +1839,7 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 	 * Parallel index scans require space in shared memory to store the
 	 * current array elements (for arrays kept by preprocessing) to schedule
 	 * the next primitive index scan.  The underlying structure is protected
-	 * using a spinlock, so defensively limit its size.  In practice this can
+	 * using an LWLock, so defensively limit its size.  In practice this can
 	 * only affect parallel scans that use an incomplete opfamily.
 	 */
 	if (scan->parallel_scan && so->numArrayKeys > INDEX_MAX_KEYS)
@@ -1575,6 +1849,189 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit every so->arrayKeys[] entry.
+ *
+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller must add this many
+ * skip arrays to each of the most significant attributes lacking any keys
+ * that use the = strategy (IS NULL keys count as = keys here).  The specific
+ * attributes that need skip arrays are indicated by initializing caller's
+ * skip_eq_ops[] 0-based attribute offset to a valid = op strategy Oid.  We'll
+ * only ever set skip_eq_ops[] entries to InvalidOid for attributes that
+ * already have an equality key in scan->keyData[] input keys -- and only when
+ * there's some later "attribute gap" for us to "fill-in" with a skip array.
+ *
+ * We're optimistic about skipping working out: we always add exactly the skip
+ * arrays needed to maximize the number of input scan keys that can ultimately
+ * be marked as required to continue the scan (but no more).  For a composite
+ * index on (a, b, c, d), we'll instruct caller to add skip arrays as follows:
+ *
+ * Input keys						Output keys (after all preprocessing)
+ * ----------						-------------------------------------
+ * a = 1							a = 1 (no skip arrays)
+ * a = ANY('{0, 1}')				a = ANY('{0, 1}') (no skip arrays)
+ * b = 42							skip a AND b = 42
+ * b = ANY('{40, 42}')				skip a AND b = ANY('{40, 42}')
+ * a = 1 AND b = 42					a = 1 AND b = 42 (no skip arrays)
+ * a >= 1 AND b = 42				range skip a AND b = 42
+ * a = 1 AND b > 42					a = 1 AND b > 42 (no skip arrays)
+ * a >= 1 AND a <= 3 AND b = 42		range skip a AND b = 42
+ * a = 1 AND c <= 27				a = 1 AND skip b AND c <= 27
+ * a = 1 AND d >= 1					a = 1 AND skip b AND skip c AND d >= 1
+ * a = 1 AND b >= 42 AND d > 1		a = 1 AND range skip b AND skip c AND d > 1
+ */
+static int
+_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_skip = 1,
+				attno_inkey = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+#ifdef DEBUG_DISABLE_SKIP_SCAN
+	/* don't attempt to add skip arrays */
+	return numArrayKeys;
+#endif
+
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+			/* Look up input opclass's equality operator (might fail) */
+			skip_eq_ops[attno_skip - 1] =
+				get_opfamily_member(opfamily, opcintype, opcintype,
+									BTEqualStrategyNumber);
+			if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key
+		 * (doesn't matter whether that attribute has an equality key, or just
+		 * uses inequality keys).
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys
+			 */
+			if (attno_has_equal)
+			{
+				/* Attributes with an = key must have InvalidOid eq_op set */
+				skip_eq_ops[attno_skip - 1] = InvalidOid;
+			}
+			else
+			{
+				Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+				Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+				/* Look up input opclass's equality operator (might fail) */
+				skip_eq_ops[attno_skip - 1] =
+					get_opfamily_member(opfamily, opcintype, opcintype,
+										BTEqualStrategyNumber);
+
+				if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+				{
+					/*
+					 * Input opclass lacks an equality strategy operator, so
+					 * don't generate a skip array that definitely won't work
+					 */
+					break;
+				}
+
+				/* plan on adding a backfill skip array for this attribute */
+				(*numSkipArrayKeys)++;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required later on.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
 /*
  * _bt_find_extreme_element() -- get least or greatest array element
  *
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index ca2a6e8f8..4e51fb8c0 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -30,6 +30,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -71,7 +72,7 @@ typedef struct BTParallelScanDescData
 									 * available for scan. see above for
 									 * possible states of parallel scan. */
 	uint64		btps_nsearches; /* counts index searches for EXPLAIN ANALYZE */
-	slock_t		btps_mutex;		/* protects above variables, btps_arrElems */
+	LWLock		btps_lock;		/* protects above variables, btps_arrElems */
 	ConditionVariable btps_cv;	/* used to synchronize parallel scan */
 
 	/*
@@ -79,11 +80,24 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 *
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -335,6 +349,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	else
 		so->keyData = NULL;
 
+	so->skipScan = false;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->oppositeDirCheck = false;
@@ -540,10 +555,155 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume that every input scankey will be output with its
+	 * own SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * array (and an associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		CompactAttribute *attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescCompactAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to a full third of a page of
+		 * space.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BLCKSZ / 3);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   array->attbyval, array->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array key, while freeing memory allocated for old
+		 * sk_argument where required
+		 */
+		if (!array->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -554,7 +714,8 @@ btinitparallelscan(void *target)
 {
 	BTParallelScanDesc bt_target = (BTParallelScanDesc) target;
 
-	SpinLockInit(&bt_target->btps_mutex);
+	LWLockInitialize(&bt_target->btps_lock,
+					 LWTRANCHE_PARALLEL_BTREE_SCAN);
 	bt_target->btps_nextScanPage = InvalidBlockNumber;
 	bt_target->btps_lastCurrPage = InvalidBlockNumber;
 	bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
@@ -577,16 +738,16 @@ btparallelrescan(IndexScanDesc scan)
 												  parallel_scan->ps_offset);
 
 	/*
-	 * In theory, we don't need to acquire the spinlock here, because there
+	 * In theory, we don't need to acquire the LWLock here, because there
 	 * shouldn't be any other workers running at this point, but we do so for
 	 * consistency.
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = InvalidBlockNumber;
 	btscan->btps_lastCurrPage = InvalidBlockNumber;
 	btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
 	/* deliberately don't reset btps_nsearches (matches index_rescan) */
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
@@ -613,6 +774,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -657,7 +819,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 
 	while (1)
 	{
-		SpinLockAcquire(&btscan->btps_mutex);
+		LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 
 		if (btscan->btps_pageStatus == BTPARALLEL_DONE)
 		{
@@ -679,14 +841,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -724,7 +881,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			*last_curr_page = btscan->btps_lastCurrPage;
 			exit_loop = true;
 		}
-		SpinLockRelease(&btscan->btps_mutex);
+		LWLockRelease(&btscan->btps_lock);
 		if (exit_loop || !status)
 			break;
 		ConditionVariableSleep(&btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
@@ -768,11 +925,11 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
 	btscan->btps_lastCurrPage = curr_page;
 	btscan->btps_pageStatus = BTPARALLEL_IDLE;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 	ConditionVariableSignal(&btscan->btps_cv);
 }
 
@@ -811,7 +968,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * Mark the parallel scan as done, unless some other process did so
 	 * already
 	 */
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	Assert(btscan->btps_pageStatus != BTPARALLEL_NEED_PRIMSCAN);
 	if (btscan->btps_pageStatus != BTPARALLEL_DONE)
 	{
@@ -824,7 +981,7 @@ _bt_parallel_done(IndexScanDesc scan)
 	 * primscan counter, which we maintain in shared memory
 	 */
 	scan->nsearches = btscan->btps_nsearches;
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 
 	/* wake up all the workers associated with this parallel scan */
 	if (status_changed)
@@ -842,6 +999,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -851,7 +1009,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
 												  parallel_scan->ps_offset);
 
-	SpinLockAcquire(&btscan->btps_mutex);
+	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
 		btscan->btps_pageStatus == BTPARALLEL_IDLE)
 	{
@@ -861,14 +1019,9 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_nsearches++;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
-	SpinLockRelease(&btscan->btps_mutex);
+	LWLockRelease(&btscan->btps_lock);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index babb530ed..3e06a260e 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -964,6 +964,15 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  All
+	 * array keys are always considered = keys, but we'll sometimes need to
+	 * treat the current key value as if we were using an inequality strategy.
+	 * This happens whenever a skip array's scan key is found to have been set
+	 * to one of the special sentinel values representing an imaginary point
+	 * before/after some (or all) of the index's real indexable values.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1039,8 +1048,56 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is MINVAL, choose low_compare (when scanning
+				 * backwards it'll be MAXVAL, and we'll choose high_compare).
+				 *
+				 * Note: if the array's low_compare key makes 'chosen' NULL,
+				 * then we behave as if the array's first element is -inf,
+				 * except when !array->null_elem implies a usable NOT NULL
+				 * constraint.
+				 */
+				if (chosen != NULL &&
+					(chosen->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skipequalitykey = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					Assert(so->skipScan);
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MAXVAL));
+						chosen = array->low_compare;
+					}
+					else
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MINVAL));
+						chosen = array->high_compare;
+					}
+
+					Assert(chosen == NULL ||
+						   chosen->sk_attno == skipequalitykey->sk_attno);
+
+					if (!array->null_elem)
+						impliesNN = skipequalitykey;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1083,9 +1140,41 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 
 				/*
-				 * Done if that was the last attribute, or if next key is not
-				 * in sequence (implying no boundary key is available for the
-				 * next attribute).
+				 * If the key that we just added to startKeys[] is a skip
+				 * array = key whose current element is marked NEXT or PRIOR,
+				 * make strat_total > or < (and stop adding boundary keys).
+				 * This can only happen with opclasses that lack skip support.
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(so->skipScan);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(strat_total == BTEqualStrategyNumber);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We're done.  We'll never find an exact = match for a
+					 * NEXT or PRIOR sentinel sk_argument value.  There's no
+					 * sense in trying to add more keys to startKeys[].
+					 */
+					break;
+				}
+
+				/*
+				 * Done if that was the last scan key output by preprocessing.
+				 * Also done if there is a gap index attribute that lacks a
+				 * usable key (only possible when preprocessing was unable to
+				 * generate a skip array key to "fill in the gap").
 				 */
 				if (i >= so->numberOfKeys ||
 					cur->sk_attno != curattr + 1)
@@ -1577,10 +1666,12 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * corresponding value from the last item on the page.  So checking with
 	 * the last item on the page would give a more precise answer.
 	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
+	 * We don't do this for the first page read by each (primitive) scan, to
+	 * avoid slowing down point queries.  They typically don't stand to gain
+	 * much when the optimization can be applied, and are more likely to
+	 * notice the overhead of the precheck.  Also avoid it during skip scans,
+	 * where individual primitive scans that don't just read one leaf page are
+	 * likely to advance their skip array(s) several times per leaf page read.
 	 *
 	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
 	 * just set a low-order required array's key to the best available match
@@ -1604,7 +1695,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !so->skipScan && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 535e27146..f61a2bade 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -30,6 +30,17 @@
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									  int32 set_elem_result, Datum tupdatum, bool tupnull);
+static void _bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_array_set_low_or_high(Relation rel, ScanKey skey,
+									  BTArrayKeyInfo *array, bool low_not_high);
+static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -207,6 +218,7 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 	int32		result = 0;
 
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
+	Assert(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)));
 
 	if (tupnull)				/* NULL tupdatum */
 	{
@@ -283,6 +295,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* SAOP arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -405,6 +419,185 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, since skip arrays don't really
+ * contain elements (they generate their array elements procedurally instead).
+ * Our interface matches that of _bt_binsrch_array_skey in every other way.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  We return -1 when tupdatum/tupnull is lower that any value
+ * within the range of the array, and 1 when it is higher than every value.
+ * Caller should pass *set_elem_result to _bt_skiparray_set_element to advance
+ * the array.
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (array->null_elem)
+	{
+		Assert(!array->low_compare && !array->high_compare);
+
+		*set_elem_result = 0;
+		return;
+	}
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+
+	/*
+	 * Assert that any keys that were assumed to be satisfied already (due to
+	 * caller passing cur_elem_trig=true) really are satisfied as expected
+	 */
+#ifdef USE_ASSERT_CHECKING
+	if (cur_elem_trig)
+	{
+		if (ScanDirectionIsForward(dir) && array->low_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												  array->low_compare->sk_collation,
+												  tupdatum,
+												  array->low_compare->sk_argument)));
+
+		if (ScanDirectionIsBackward(dir) && array->high_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												  array->high_compare->sk_collation,
+												  tupdatum,
+												  array->high_compare->sk_argument)));
+	}
+#endif
+}
+
+/*
+ * _bt_skiparray_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Caller passes set_elem_result returned by _bt_binsrch_skiparray_skey for
+ * caller's tupdatum/tupnull.
+ *
+ * We copy tupdatum/tupnull into skey's sk_argument iff set_elem_result == 0.
+ * Otherwise, we set skey to either the lowest or highest value that's within
+ * the range of caller's skip array (whichever is the best available match to
+ * tupdatum/tupnull that is still within the range of the skip array according
+ * to _bt_binsrch_skiparray_skey/set_elem_result).
+ */
+static void
+_bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  int32 set_elem_result, Datum tupdatum, bool tupnull)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (set_elem_result)
+	{
+		/* tupdatum/tupnull is out of the range of the skip array */
+		Assert(!array->null_elem);
+
+		_bt_array_set_low_or_high(rel, skey, array, set_elem_result < 0);
+		return;
+	}
+
+	/* Advance skip array to tupdatum (or tupnull) value */
+	if (unlikely(tupnull))
+	{
+		_bt_skiparray_set_isnull(rel, skey, array);
+		return;
+	}
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen);
+}
+
+/*
+ * _bt_skiparray_set_isnull() -- set skip array scan key to NULL
+ */
+static void
+_bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->null_elem && !array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -414,29 +607,349 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_array_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_array_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  bool low_not_high)
+{
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting array to lowest element (according to low_compare) */
+		skey->sk_flags |= SK_BT_MINVAL;
+	}
+	else
+	{
+		/* Setting array to highest element (according to high_compare) */
+		skey->sk_flags |= SK_BT_MAXVAL;
+	}
+}
+
+/*
+ * _bt_array_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the minimum value within the range
+	 * of a skip array (often just -inf) is never decrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly decrement sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Decrement" from NULL to the high_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->high_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup->decrement(rel, skey->sk_argument, &uflow);
+	if (unlikely(uflow))
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Decrement" sk_argument to NULL */
+			_bt_skiparray_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	return true;
+}
+
+/*
+ * _bt_array_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MINVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the maximum value within the range
+	 * of a skip array (often just +inf) is never incrementable
+	 */
+	if (skey->sk_flags & SK_BT_MAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly increment sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Increment" from NULL to the low_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->low_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup->increment(rel, skey->sk_argument, &oflow);
+	if (unlikely(oflow))
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			/* "Increment" sk_argument to NULL */
+			_bt_skiparray_set_isnull(rel, skey, array);
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -452,6 +965,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -461,29 +975,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_array_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_array_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -507,7 +1022,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 }
 
 /*
- * _bt_rewind_nonrequired_arrays() -- Rewind non-required arrays
+ * _bt_rewind_nonrequired_arrays() -- Rewind SAOP arrays not marked required
  *
  * Called when _bt_advance_array_keys decides to start a new primitive index
  * scan on the basis of the current scan position being before the position
@@ -539,10 +1054,15 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
  *
  * Note: _bt_verify_arrays_bt_first is called by an assertion to enforce that
  * everybody got this right.
+ *
+ * Note: In practice almost all SAOP arrays are marked required during
+ * preprocessing (if necessary by generating skip arrays).  It is hardly ever
+ * truly necessary to call here, but consistently doing so is simpler.
  */
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -550,7 +1070,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -562,16 +1081,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_array_set_low_or_high(rel, cur, array,
+								  ScanDirectionIsForward(dir));
 	}
 }
 
@@ -696,9 +1209,77 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (likely(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))))
+		{
+			/* Scankey has a valid/comparable sk_argument value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0)
+			{
+				/*
+				 * Interpret result in a way that takes NEXT/PRIOR into
+				 * account
+				 */
+				if (cur->sk_flags & SK_BT_NEXT)
+					result = -1;
+				else if (cur->sk_flags & SK_BT_PRIOR)
+					result = 1;
+
+				Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP));
+			}
+		}
+		else
+		{
+			BTArrayKeyInfo *array = NULL;
+
+			/*
+			 * Current array element/array = scan key value is a sentinel
+			 * value that represents the lowest (or highest) possible value
+			 * that's still within the range of the array.
+			 *
+			 * Like _bt_first, we only see MINVAL keys during forwards scans
+			 * (and similarly only see MAXVAL keys during backwards scans).
+			 * Even if the scan's direction changes, we'll stop at some higher
+			 * order key before we can ever reach any MAXVAL (or MINVAL) keys.
+			 * (However, unlike _bt_first we _can_ get to keys marked either
+			 * NEXT or PRIOR, regardless of the scan's current direction.)
+			 */
+			Assert(ScanDirectionIsForward(dir) ?
+				   !(cur->sk_flags & SK_BT_MAXVAL) :
+				   !(cur->sk_flags & SK_BT_MINVAL));
+
+			/*
+			 * There are no valid sk_argument values in MINVAL/MAXVAL keys.
+			 * Check if tupdatum is within the range of skip array instead.
+			 */
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			_bt_binsrch_skiparray_skey(false, dir, tupdatum, tupnull,
+									   array, cur, &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum satisfies both low_compare and high_compare, so
+				 * it's time to advance the array keys.
+				 *
+				 * Note: It's possible that the skip array will "advance" from
+				 * its MINVAL (or MAXVAL) representation to an alternative,
+				 * logically equivalent representation of the same value: a
+				 * representation where the = key gets a valid datum in its
+				 * sk_argument.  This is only possible when low_compare uses
+				 * the >= strategy (or high_compare uses the <= strategy).
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1017,18 +1598,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1053,18 +1625,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -1080,14 +1643,22 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			bool		cur_elem_trig = (sktrig_required && ikey == sktrig);
 
 			/*
-			 * Binary search for closest match that's available from the array
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems == -1)
+				_bt_binsrch_skiparray_skey(cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * Binary search for the closest match from the SAOP array
+			 */
+			else
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 		}
 		else
 		{
@@ -1163,11 +1734,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+		if (array)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->num_elems == -1)
+			{
+				/* Skip array's new element is tupdatum (or MINVAL/MAXVAL) */
+				_bt_skiparray_set_element(rel, cur, array, result,
+										  tupdatum, tupnull);
+			}
+			else if (array->cur_elem != set_elem)
+			{
+				/* SAOP array's new element is set_elem datum */
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
 		}
 	}
 
@@ -1586,10 +2167,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -1920,6 +2502,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key uses one of several sentinel values.  We just
+		 * fall back on _bt_tuple_before_array_skeys when we see such a value.
+		 */
+		if (key->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL |
+							 SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index dd6f5a15c..817ad358f 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -106,6 +106,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 8546366ee..a6dd8eab5 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("ordering equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (GetIndexAmRoutineByAmId(amoid, false)->amcanhash)
 	{
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 8adf27302..5702c35bb 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -153,6 +153,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_LOCK_MANAGER] = "LockManager",
 	[LWTRANCHE_PREDICATE_LOCK_MANAGER] = "PredicateLockManager",
 	[LWTRANCHE_PARALLEL_HASH_JOIN] = "ParallelHashJoin",
+	[LWTRANCHE_PARALLEL_BTREE_SCAN] = "ParallelBtreeScan",
 	[LWTRANCHE_PARALLEL_QUERY_DSA] = "ParallelQueryDSA",
 	[LWTRANCHE_PER_SESSION_DSA] = "PerSessionDSA",
 	[LWTRANCHE_PER_SESSION_RECORD_TYPE] = "PerSessionRecordType",
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index e199f0716..1d3d901dc 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -371,6 +371,7 @@ BufferMapping	"Waiting to associate a data block with a buffer in the buffer poo
 LockManager	"Waiting to read or update information about <quote>heavyweight</quote> locks."
 PredicateLockManager	"Waiting to access predicate lock information used by serializable transactions."
 ParallelHashJoin	"Waiting to synchronize workers during Parallel Hash Join plan execution."
+ParallelBtreeScan	"Waiting to synchronize workers during a parallel B-tree scan."
 ParallelQueryDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionDSA	"Waiting for parallel query dynamic shared memory allocation."
 PerSessionRecordType	"Waiting to access a parallel query's information about composite types."
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index f279853de..4227ab1a7 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f23cfad71..244f48f4f 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index c2918c9c8..66cb5fc64 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -94,7 +94,6 @@
 
 #include "postgres.h"
 
-#include <ctype.h>
 #include <math.h>
 
 #include "access/brin.h"
@@ -193,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -214,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5768,6 +5771,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6826,6 +6915,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6835,17 +6971,21 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
+	double		correlation = 0;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
+	bool		set_correlation = false;
 	bool		eqQualHere;
-	bool		found_saop;
+	bool		found_array;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	bool		upper_inequal_col;
+	bool		lower_inequal_col;
 	double		num_sa_scans;
+	double		inequalselectivity;
 	ListCell   *lc;
 
 	/*
@@ -6856,21 +6996,32 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * it's OK to count them in indexSelectivity, but they should not count
 	 * for estimating numIndexTuples.  So we must examine the given indexquals
 	 * to find out which ones count as boundary quals.  We rely on the
-	 * knowledge that they are given in index column order.
+	 * knowledge that they are given in index column order (though this
+	 * process is complicated by the use of skip arrays, as explained below).
 	 *
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
+	 * If there's a ScalarArrayOp array in the quals, or if B-Tree
+	 * preprocessing will be able to generate a skip array, we'll actually
+	 * perform up to N index descents (not just one), but the underlying
 	 * operator can be considered to act the same as it normally does.
+	 *
+	 * In practice, non-leading quals often _can_ act as boundary quals due to
+	 * preprocessing generating a "bridging" skip array.  Whether or not we'll
+	 * actually treat lower-order quals as boundary quals (that is, quals that
+	 * influence our numIndexTuples estimate) is determined by heuristics.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
-	found_saop = false;
+	found_array = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
+	upper_inequal_col = false;
+	lower_inequal_col = false;
 	num_sa_scans = 1;
+	inequalselectivity = 1;
 	foreach(lc, path->indexclauses)
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
@@ -6878,13 +7029,106 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		if (indexcol != iclause->indexcol)
 		{
+			bool		past_first_skipped = false;
+
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
-			eqQualHere = false;
-			indexcol++;
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't come after a RowCompare */
+
+			/*
+			 * Now estimate number of "array elements" using ndistinct.
+			 *
+			 * Internally, nbtree treats skip scans as scans with SAOP style
+			 * arrays that generate elements procedurally.  We effectively
+			 * assume a "col = ANY('{every possible col value}')" qual.
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT,
+							new_num_sa_scans;
+				bool		isdefault = true;
+
+				/* Attain ndistinct for index column/indexed expression */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						Assert(!set_correlation);
+						correlation = btcost_correlation(index, &vardata);
+						set_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				if (!past_first_skipped)
+				{
+					/*
+					 * Apply the selectivities of any inequalities to
+					 * ndistinct (unless ndistinct is only a default estimate)
+					 */
+					if (!isdefault)
+						ndistinct *= inequalselectivity;
+
+					/*
+					 * Skip scan will likely require an initial index descent
+					 * to find out what the real first element is...
+					 */
+					if (!upper_inequal_col)
+						ndistinct += 1;
+
+					/*
+					 * ...and another extra descent to confirm no further
+					 * groupings/matches
+					 */
+					if (!lower_inequal_col)
+						ndistinct += 1;
+
+					past_first_skipped = true;
+				}
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * relying on skipping.
+				 */
+				found_array = true;
+				new_num_sa_scans = num_sa_scans * ndistinct;
+
+				/*
+				 * Stop adding new skip arrays when the would-be new
+				 * num_sa_scans exceeds a third of the number of index pages
+				 */
+				if (ceil(index->pages * 0.3333333) < new_num_sa_scans)
+				{
+					/* Qual (and later quals) won't affect numIndexTuples */
+					break;
+				}
+
+				/* Done counting skip array "elements" for this column */
+				num_sa_scans = new_num_sa_scans;
+				indexcol++;
+			}
+
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+				break;			/* no quals at all for indexcol (can't skip) */
+
+			/* reset for next indexcol */
+			eqQualHere = false;
+			upper_inequal_col = false;
+			lower_inequal_col = false;
+			inequalselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6906,6 +7150,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6914,7 +7159,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				double		alength = estimate_array_length(root, other_operand);
 
 				clause_op = saop->opno;
-				found_saop = true;
+				found_array = true;
 				/* estimate SA descents by indexBoundQuals only */
 				if (alength > 1)
 					num_sa_scans *= alength;
@@ -6926,7 +7171,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skip scan purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6942,6 +7187,34 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+				else if (rinfo->norm_selec >= 0)
+				{
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct.
+					 *
+					 * Assume inequality selectivities are _not_ independent,
+					 * but only track up to one upper bound inequality and up
+					 * to one lower bound inequality.  This avoids wildly
+					 * wrong estimates given redundant operators.
+					 */
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (!upper_inequal_col)
+							inequalselectivity =
+								Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+									DEFAULT_RANGE_INEQ_SEL);
+						upper_inequal_col = true;
+					}
+					else
+					{
+						if (!lower_inequal_col)
+							inequalselectivity =
+								Max(inequalselectivity - (1.0 - rinfo->norm_selec),
+									DEFAULT_RANGE_INEQ_SEL);
+						lower_inequal_col = true;
+					}
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6951,13 +7224,13 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	/*
 	 * If index is unique and we found an '=' clause for each column, we can
 	 * just assume numIndexTuples = 1 and skip the expensive
-	 * clauselist_selectivity calculations.  However, a ScalarArrayOp or
-	 * NullTest invalidates that theory, even though it sets eqQualHere.
+	 * clauselist_selectivity calculations.  However, an array or NullTest
+	 * invalidates that theory, even though it sets eqQualHere.
 	 */
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
-		!found_saop &&
+		!found_array &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
 	else
@@ -6979,11 +7252,11 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		numIndexTuples = btreeSelectivity * index->rel->tuples;
 
 		/*
-		 * btree automatically combines individual ScalarArrayOpExpr primitive
-		 * index scans whenever the tuples covered by the next set of array
-		 * keys are close to tuples covered by the current set.  That puts a
-		 * natural ceiling on the worst case number of descents -- there
-		 * cannot possibly be more than one descent per leaf page scanned.
+		 * btree automatically combines individual array primitive index scans
+		 * whenever the tuples covered by the next set of array keys are close
+		 * to tuples covered by the current set.  That puts a natural ceiling
+		 * on the worst case number of descents -- there cannot possibly be
+		 * more than one descent per leaf page scanned.
 		 *
 		 * Clamp the number of descents to at most 1/3 the number of index
 		 * pages.  This avoids implausibly high estimates with low selectivity
@@ -6997,16 +7270,18 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * of leaf pages (we make it 1/3 the total number of pages instead) to
 		 * give the btree code credit for its ability to continue on the leaf
 		 * level with low selectivity scans.
+		 *
+		 * Note: num_sa_scans includes both ScalarArrayOp array elements and
+		 * skip array elements whose qual affects our numIndexTuples estimate.
 		 */
 		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
-		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
-		 * to integer.
+		 * As in genericcostestimate(), we have to adjust for any array quals
+		 * included in indexBoundQuals, and then round to integer.
 		 *
-		 * It is tempting to make genericcostestimate behave as if SAOP
+		 * It is tempting to make genericcostestimate behave as if array
 		 * clauses work in almost the same way as scalar operators during
 		 * btree scans, making the top-level scan look like a continuous scan
 		 * (as opposed to num_sa_scans-many primitive index scans).  After
@@ -7059,110 +7334,25 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * cost is somewhat arbitrarily set at 50x cpu_operator_cost per page
 	 * touched.  The number of such pages is btree tree height plus one (ie,
 	 * we charge for the leaf page too).  As above, charge once per estimated
-	 * SA index descent.
+	 * index descent.
 	 */
 	descentCost = (index->tree_height + 1) * DEFAULT_PAGE_CPU_MULTIPLIER * cpu_operator_cost;
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!set_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..2bd35d2d2
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns skip support struct, allocating in caller's memory
+ * context.  Otherwise returns NULL, indicating that operator class has no
+ * skip support function.
+ */
+SkipSupport
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse)
+{
+	Oid			skipSupportFunction;
+	SkipSupport sksup;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return NULL;
+
+	sksup = palloc(sizeof(SkipSupportData));
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return sksup;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 9682f9dbd..347089b76 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2304,6 +2305,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 4f8402ef9..1b72059d2 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,6 +13,7 @@
 
 #include "postgres.h"
 
+#include <limits.h>
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
@@ -21,6 +22,7 @@
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -416,6 +418,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index c50ba60e2..c8df09f85 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -833,7 +833,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 55e3b382f..5f7b87f84 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4242,7 +4242,10 @@ description | Waiting for a newly initialized WAL file to reach durable storage
      <replaceable>value1</replaceable> OR
      <replaceable>expression</replaceable> = <replaceable>value2</replaceable>
      ...</literal> constructs when the query planner transforms the constructs
-    into an equivalent multi-valued array representation.
+    into an equivalent multi-valued array representation.  Similarly, when
+    B-Tree index scans use the skip scan strategy, an index search is
+    performed each time the scan is repositioned to the next index leaf page
+    that might have matching tuples.
    </para>
   </note>
   <tip>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 053619624..7e23a7b6e 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index 0c274d56a..23bf33f10 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  ordering equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 8879554c3..bfb1a286e 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -581,6 +581,47 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Index Only Scan using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Index Only Scan Backward using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 --
 -- Test for multilevel page deletion
 --
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index bd5f002cf..15dde752f 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1637,7 +1637,9 @@ DROP TABLE syscol_table;
 -- Tests for IS NULL/IS NOT NULL with b-tree indexes
 --
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 SET enable_seqscan = OFF;
 SET enable_indexscan = ON;
@@ -1645,7 +1647,7 @@ SET enable_bitmapscan = ON;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1657,13 +1659,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1678,12 +1680,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1695,13 +1703,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1722,12 +1730,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0,
      1
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc nulls last,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1739,13 +1753,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1760,12 +1774,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2  nulls first,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1777,13 +1797,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1798,6 +1818,12 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 -- Check initial-positioning logic too
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2);
@@ -1829,20 +1855,24 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
 (2 rows)
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-         |        
-     278 |     999
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 5;
+ unique1 |  unique2   
+---------+------------
+     500 |           
+     100 |           
+         |           
+         | 2147483647
+     278 |        999
+(5 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-     278 |     999
-       0 |     998
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 3;
+ unique1 |  unique2   
+---------+------------
+         | 2147483647
+     278 |        999
+       0 |        998
+(3 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
@@ -2247,7 +2277,8 @@ SELECT count(*) FROM dupindexcols
 (1 row)
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 explain (costs off)
 SELECT unique1 FROM tenk1
@@ -2269,7 +2300,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2289,7 +2320,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2309,6 +2340,25 @@ ORDER BY thousand DESC, tenthous DESC;
         0 |     3000
 (2 rows)
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ Index Only Scan Backward using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > 995) AND (tenthous = ANY ('{998,999}'::integer[])))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+ thousand | tenthous 
+----------+----------
+      999 |      999
+      998 |      998
+(2 rows)
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -2339,6 +2389,45 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 ---------
 (0 rows)
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+                   QUERY PLAN                    
+-------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY (NULL::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY ('{NULL,NULL,NULL}'::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: ((unique1 IS NULL) AND (unique1 IS NULL))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+ unique1 
+---------
+(0 rows)
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
                                 QUERY PLAN                                 
@@ -2462,6 +2551,44 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 ---------
 (0 rows)
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > '-1'::integer) AND (thousand >= 0) AND (tenthous = 3000))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+(1 row)
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand < 3) AND (thousand <= 2) AND (tenthous = 1001))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        1 |     1001
+(1 row)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 6543e90de..6ebd6265f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5331,9 +5331,10 @@ Function              | in_range(time without time zone,time without time zone,i
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql
index 670ad5c6e..68c61dbc7 100644
--- a/src/test/regress/sql/btree_index.sql
+++ b/src/test/regress/sql/btree_index.sql
@@ -327,6 +327,27 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 
 --
 -- Test for multilevel page deletion
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index be570da08..40ba3d65b 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -650,7 +650,9 @@ DROP TABLE syscol_table;
 --
 
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 
 SET enable_seqscan = OFF;
@@ -663,6 +665,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -675,6 +678,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NUL
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0, 1);
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -686,6 +690,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -697,6 +702,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -716,9 +722,9 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
   ORDER BY unique2 LIMIT 2;
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 5;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 3;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
 
@@ -852,7 +858,8 @@ SELECT count(*) FROM dupindexcols
   WHERE f1 BETWEEN 'WA' AND 'ZZZ' and id < 1000 and f1 ~<~ 'YX';
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 
 explain (costs off)
@@ -864,7 +871,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -874,7 +881,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -884,6 +891,15 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand DESC, tenthous DESC;
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -897,6 +913,21 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 
 SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('{33, 44}'::bigint[]);
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
 
@@ -942,6 +973,26 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
 SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index cfbab589d..17a835728 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -220,6 +220,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2699,6 +2700,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.47.2

v26-0002-Improve-nbtree-SAOP-primitive-scan-scheduling.patchapplication/octet-stream; name=v26-0002-Improve-nbtree-SAOP-primitive-scan-scheduling.patchDownload
From b356a16a067aa34640328f0c582fba9dca7ba445 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Fri, 14 Feb 2025 16:11:59 -0500
Subject: [PATCH v26 2/6] Improve nbtree SAOP primitive scan scheduling.

Add new primitive index scan scheduling heuristics that make
_bt_advance_array_keys avoid ending the ongoing primitive index scan
when it has already read more than one leaf page during the ongoing
primscan.  This tends to result in better decisions about how to start
and end primitive index scans with queries that have SAOP arrays with
many elements that are clustered together (e.g., contiguous integers).

Preparation for an upcoming patch that will add skip scan optimizations
to nbtree.  That'll work by adding skip arrays, which behave similarly
to SAOP arrays with many elements clustered together.

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wzkz0wPe6+02kr+hC+JJNKfGtjGTzpG3CFVTQmKwWNrXNw@mail.gmail.com
---
 src/include/access/nbtree.h           |   9 +-
 src/backend/access/nbtree/nbtsearch.c |  75 +++++----
 src/backend/access/nbtree/nbtutils.c  | 227 ++++++++++++++------------
 3 files changed, 173 insertions(+), 138 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index e4fdeca34..947431954 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1043,8 +1043,8 @@ typedef struct BTScanOpaqueData
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
-	bool		scanBehind;		/* Last array advancement matched -inf attr? */
-	bool		oppositeDirCheck;	/* explicit scanBehind recheck needed? */
+	bool		scanBehind;		/* arrays might be ahead of scan's key space? */
+	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
 	BTArrayKeyInfo *arrayKeys;	/* info about each equality-type array key */
 	FmgrInfo   *orderProcs;		/* ORDER procs for required equality keys */
 	MemoryContext arrayContext; /* scan-lifespan context for array data */
@@ -1099,6 +1099,7 @@ typedef struct BTReadPageState
 	 * Input and output parameters, set and unset by both _bt_readpage and
 	 * _bt_checkkeys to manage precheck optimizations
 	 */
+	bool		firstpage;		/* on first page of current primitive scan? */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
 
@@ -1298,8 +1299,8 @@ extern int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir);
 extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 						  IndexTuple tuple, int tupnatts);
-extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
-								  IndexTuple finaltup);
+extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
+									 IndexTuple finaltup);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 941b4eaaf..babb530ed 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -33,7 +33,7 @@ static OffsetNumber _bt_binsrch(Relation rel, BTScanInsert key, Buffer buf);
 static int	_bt_binsrch_posting(BTScanInsert key, Page page,
 								OffsetNumber offnum);
 static bool _bt_readpage(IndexScanDesc scan, ScanDirection dir,
-						 OffsetNumber offnum, bool firstPage);
+						 OffsetNumber offnum, bool firstpage);
 static void _bt_saveitem(BTScanOpaque so, int itemIndex,
 						 OffsetNumber offnum, IndexTuple itup);
 static int	_bt_setuppostingitems(BTScanOpaque so, int itemIndex,
@@ -1499,7 +1499,7 @@ _bt_next(IndexScanDesc scan, ScanDirection dir)
  */
 static bool
 _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
-			 bool firstPage)
+			 bool firstpage)
 {
 	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -1558,6 +1558,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.offnum = InvalidOffsetNumber;
 	pstate.skip = InvalidOffsetNumber;
 	pstate.continuescan = true; /* default assumption */
+	pstate.firstpage = firstpage;
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
 	pstate.rechecks = 0;
@@ -1603,7 +1604,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!firstPage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
@@ -1620,36 +1621,29 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	if (ScanDirectionIsForward(dir))
 	{
 		/* SK_SEARCHARRAY forward scans must provide high key up front */
-		if (arrayKeys && !P_RIGHTMOST(opaque))
+		if (arrayKeys)
 		{
-			ItemId		iid = PageGetItemId(page, P_HIKEY);
-
-			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
-
-			if (unlikely(so->oppositeDirCheck))
+			if (!P_RIGHTMOST(opaque))
 			{
-				Assert(so->scanBehind);
+				ItemId		iid = PageGetItemId(page, P_HIKEY);
 
-				/*
-				 * Last _bt_readpage call scheduled a recheck of finaltup for
-				 * required scan keys up to and including a > or >= scan key.
-				 *
-				 * _bt_checkkeys won't consider the scanBehind flag unless the
-				 * scan is stopped by a scan key required in the current scan
-				 * direction.  We need this recheck so that we'll notice when
-				 * all tuples on this page are still before the _bt_first-wise
-				 * start of matches for the current set of array keys.
-				 */
-				if (!_bt_oppodir_checkkeys(scan, dir, pstate.finaltup))
+				pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+				/* If a recheck is scheduled, check finaltup first */
+				if (unlikely(so->scanBehind) &&
+					!_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup))
 				{
 					/* Schedule another primitive index scan after all */
 					so->currPos.moreRight = false;
 					so->needPrimScan = true;
+					if (scan->parallel_scan)
+						_bt_parallel_primscan_schedule(scan,
+													   so->currPos.currPage);
 					return false;
 				}
-
-				/* Deliberately don't unset scanBehind flag just yet */
 			}
+
+			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
 		/* load items[] in ascending order */
@@ -1745,7 +1739,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 		 * only appear on non-pivot tuples on the right sibling page are
 		 * common.
 		 */
-		if (pstate.continuescan && !P_RIGHTMOST(opaque))
+		if (pstate.continuescan && !so->scanBehind && !P_RIGHTMOST(opaque))
 		{
 			ItemId		iid = PageGetItemId(page, P_HIKEY);
 			IndexTuple	itup = (IndexTuple) PageGetItem(page, iid);
@@ -1767,11 +1761,29 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	else
 	{
 		/* SK_SEARCHARRAY backward scans must provide final tuple up front */
-		if (arrayKeys && minoff <= maxoff && !P_LEFTMOST(opaque))
+		if (arrayKeys)
 		{
-			ItemId		iid = PageGetItemId(page, minoff);
+			if (minoff <= maxoff && !P_LEFTMOST(opaque))
+			{
+				ItemId		iid = PageGetItemId(page, minoff);
 
-			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+				pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+				/* If a recheck is scheduled, check finaltup first */
+				if (unlikely(so->scanBehind) &&
+					!_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup))
+				{
+					/* Schedule another primitive index scan after all */
+					so->currPos.moreLeft = false;
+					so->needPrimScan = true;
+					if (scan->parallel_scan)
+						_bt_parallel_primscan_schedule(scan,
+													   so->currPos.currPage);
+					return false;
+				}
+			}
+
+			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
 		/* load items[] in descending order */
@@ -2184,7 +2196,9 @@ _bt_readfirstpage(IndexScanDesc scan, OffsetNumber offnum, ScanDirection dir)
  * scan.  A seized=false caller's blkno can never be assumed to be the page
  * that must be read next during a parallel scan, though.  We must figure that
  * part out for ourselves by seizing the scan (the correct page to read might
- * already be beyond the seized=false caller's blkno during a parallel scan).
+ * already be beyond the seized=false caller's blkno during a parallel scan,
+ * unless blkno/so->currPos.nextPage/so->currPos.prevPage is already P_NONE,
+ * or unless so->currPos.moreRight/so->currPos.moreLeft is already unset).
  *
  * On success exit, so->currPos is updated to contain data from the next
  * interesting page, and we return true.  We hold a pin on the buffer on
@@ -2205,6 +2219,7 @@ _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno,
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	Assert(so->currPos.currPage == lastcurrblkno || seized);
+	Assert(!(blkno == P_NONE && seized));
 	Assert(!BTScanPosIsPinned(so->currPos));
 
 	/*
@@ -2272,14 +2287,14 @@ _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno,
 			if (ScanDirectionIsForward(dir))
 			{
 				/* note that this will clear moreRight if we can stop */
-				if (_bt_readpage(scan, dir, P_FIRSTDATAKEY(opaque), false))
+				if (_bt_readpage(scan, dir, P_FIRSTDATAKEY(opaque), seized))
 					break;
 				blkno = so->currPos.nextPage;
 			}
 			else
 			{
 				/* note that this will clear moreLeft if we can stop */
-				if (_bt_readpage(scan, dir, PageGetMaxOffsetNumber(page), false))
+				if (_bt_readpage(scan, dir, PageGetMaxOffsetNumber(page), seized))
 					break;
 				blkno = so->currPos.prevPage;
 			}
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 693e43c67..535e27146 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -42,6 +42,8 @@ static bool _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 static bool _bt_verify_arrays_bt_first(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_verify_keys_with_arraykeys(IndexScanDesc scan);
 #endif
+static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
 							  bool advancenonrequired, bool prechecked, bool firstmatch,
@@ -870,15 +872,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	int			arrayidx = 0;
 	bool		beyond_end_advance = false,
 				has_required_opposite_direction_only = false,
-				oppodir_inequality_sktrig = false,
 				all_required_satisfied = true,
 				all_satisfied = true;
 
-	/*
-	 * Unset so->scanBehind (and so->oppositeDirCheck) in case they're still
-	 * set from back when we dealt with the previous page's high key/finaltup
-	 */
-	so->scanBehind = so->oppositeDirCheck = false;
+	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	if (sktrig_required)
 	{
@@ -990,18 +987,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			beyond_end_advance = true;
 			all_satisfied = all_required_satisfied = false;
 
-			/*
-			 * Set a flag that remembers that this was an inequality required
-			 * in the opposite scan direction only, that nevertheless
-			 * triggered the call here.
-			 *
-			 * This only happens when an inequality operator (which must be
-			 * strict) encounters a group of NULLs that indicate the end of
-			 * non-NULL values for tuples in the current scan direction.
-			 */
-			if (unlikely(required_opposite_direction_only))
-				oppodir_inequality_sktrig = true;
-
 			continue;
 		}
 
@@ -1306,10 +1291,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * Note: we don't just quit at this point when all required scan keys were
 	 * found to be satisfied because we need to consider edge-cases involving
 	 * scan keys required in the opposite direction only; those aren't tracked
-	 * by all_required_satisfied. (Actually, oppodir_inequality_sktrig trigger
-	 * scan keys are tracked by all_required_satisfied, since it's convenient
-	 * for _bt_check_compare to behave as if they are required in the current
-	 * scan direction to deal with NULLs.  We'll account for that separately.)
+	 * by all_required_satisfied.
 	 */
 	Assert(_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts,
 										false, 0, NULL) ==
@@ -1343,7 +1325,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	/*
 	 * When we encounter a truncated finaltup high key attribute, we're
 	 * optimistic about the chances of its corresponding required scan key
-	 * being satisfied when we go on to check it against tuples from this
+	 * being satisfied when we go on to recheck it against tuples from this
 	 * page's right sibling leaf page.  We consider truncated attributes to be
 	 * satisfied by required scan keys, which allows the primitive index scan
 	 * to continue to the next leaf page.  We must set so->scanBehind to true
@@ -1365,28 +1347,24 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 *
 	 * You can think of this as a speculative bet on what the scan is likely
 	 * to find on the next page.  It's not much of a gamble, though, since the
-	 * untruncated prefix of attributes must strictly satisfy the new qual
-	 * (though it's okay if any non-required scan keys fail to be satisfied).
+	 * untruncated prefix of attributes must strictly satisfy the new qual.
 	 */
-	if (so->scanBehind && has_required_opposite_direction_only)
+	if (so->scanBehind)
 	{
 		/*
-		 * However, we need to work harder whenever the scan involves a scan
-		 * key required in the opposite direction to the scan only, along with
-		 * a finaltup with at least one truncated attribute that's associated
-		 * with a scan key marked required (required in either direction).
+		 * Truncated high key -- _bt_scanbehind_checkkeys recheck scheduled.
 		 *
-		 * _bt_check_compare simply won't stop the scan for a scan key that's
-		 * marked required in the opposite scan direction only.  That leaves
-		 * us without an automatic way of reconsidering any opposite-direction
-		 * inequalities if it turns out that starting a new primitive index
-		 * scan will allow _bt_first to skip ahead by a great many leaf pages.
-		 *
-		 * We deal with this by explicitly scheduling a finaltup recheck on
-		 * the right sibling page.  _bt_readpage calls _bt_oppodir_checkkeys
-		 * for next page's finaltup (and we skip it for this page's finaltup).
+		 * Remember if recheck needs to call _bt_oppodir_checkkeys for next
+		 * page's finaltup (see below comments about "Handle inequalities
+		 * marked required in the opposite scan direction" for why).
 		 */
-		so->oppositeDirCheck = true;	/* recheck next page's high key */
+		so->oppositeDirCheck = has_required_opposite_direction_only;
+
+		/*
+		 * Make sure that any SAOP arrays that were not marked required by
+		 * preprocessing are reset to their first element for this direction
+		 */
+		_bt_rewind_nonrequired_arrays(scan, dir);
 	}
 
 	/*
@@ -1411,11 +1389,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * (primitive) scan.  If this happens at the start of a large group of
 	 * NULL values, then we shouldn't expect to be called again until after
 	 * the scan has already read indefinitely-many leaf pages full of tuples
-	 * with NULL suffix values.  We need a separate test for this case so that
-	 * we don't miss our only opportunity to skip over such a group of pages.
-	 * (_bt_first is expected to skip over the group of NULLs by applying a
-	 * similar "deduce NOT NULL" rule, where it finishes its insertion scan
-	 * key by consing up an explicit SK_SEARCHNOTNULL key.)
+	 * with NULL suffix values.  (_bt_first is expected to skip over the group
+	 * of NULLs by applying a similar "deduce NOT NULL" rule of its own, which
+	 * involves consing up an explicit SK_SEARCHNOTNULL key.)
 	 *
 	 * Apply a test against finaltup to detect and recover from the problem:
 	 * if even finaltup doesn't satisfy such an inequality, we just skip by
@@ -1423,20 +1399,18 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * that all of the tuples on the current page following caller's tuple are
 	 * also before the _bt_first-wise start of tuples for our new qual.  That
 	 * at least suggests many more skippable pages beyond the current page.
-	 * (when so->oppositeDirCheck was set, this'll happen on the next page.)
+	 * (when so->scanBehind and so->oppositeDirCheck are set, this'll happen
+	 * when we test the next page's finaltup/high key instead.)
 	 */
 	else if (has_required_opposite_direction_only && pstate->finaltup &&
-			 (all_required_satisfied || oppodir_inequality_sktrig) &&
 			 unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup)))
 	{
-		/*
-		 * Make sure that any non-required arrays are set to the first array
-		 * element for the current scan direction
-		 */
 		_bt_rewind_nonrequired_arrays(scan, dir);
 		goto new_prim_scan;
 	}
 
+continue_scan:
+
 	/*
 	 * Stick with the ongoing primitive index scan for now.
 	 *
@@ -1458,8 +1432,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	if (so->scanBehind)
 	{
 		/* Optimization: skip by setting "look ahead" mechanism's offnum */
-		Assert(ScanDirectionIsForward(dir));
-		pstate->skip = pstate->maxoff + 1;
+		if (ScanDirectionIsForward(dir))
+			pstate->skip = pstate->maxoff + 1;
+		else
+			pstate->skip = pstate->minoff - 1;
 	}
 
 	/* Caller's tuple doesn't match the new qual */
@@ -1469,6 +1445,41 @@ new_prim_scan:
 
 	Assert(pstate->finaltup);	/* not on rightmost/leftmost page */
 
+	/*
+	 * Looks like another primitive index scan is required.  But consider
+	 * backing out and continuing the primscan based on scan-level heuristics.
+	 *
+	 * Continue the ongoing primitive scan (but schedule a recheck for when
+	 * the scan arrives on the next sibling leaf page) when it has already
+	 * read at least one leaf page before the one we're reading now.  This is
+	 * important when reading subsets of an index with many distinct values in
+	 * respect of an attribute constrained by an array.  It encourages fewer,
+	 * larger primitive scans where that makes sense.
+	 *
+	 * Note: so->scanBehind is primarily used to indicate that the scan
+	 * encountered a finaltup that "satisfied" one or more required scan keys
+	 * on a truncated attribute value/-inf value.  We can safely reuse it to
+	 * force the scan to stay on the leaf level because the considerations are
+	 * exactly the same.
+	 *
+	 * Note: This heuristic isn't as aggressive as you might think.  We're
+	 * conservative about allowing a primitive scan to step from the first
+	 * leaf page it reads to the page's sibling page (we only allow it on
+	 * first pages whose finaltup strongly suggests that it'll work out).
+	 * Clearing this first page finaltup hurdle is a strong signal in itself.
+	 */
+	if (!pstate->firstpage)
+	{
+		/* Schedule a recheck once on the next (or previous) page */
+		so->scanBehind = true;
+		so->oppositeDirCheck = has_required_opposite_direction_only;
+
+		_bt_rewind_nonrequired_arrays(scan, dir);
+
+		/* Continue the current primitive scan after all */
+		goto continue_scan;
+	}
+
 	/*
 	 * End this primitive index scan, but schedule another.
 	 *
@@ -1499,7 +1510,7 @@ end_toplevel_scan:
 	 * first positions for what will then be the current scan direction.
 	 */
 	pstate->continuescan = false;	/* Tell _bt_readpage we're done... */
-	so->needPrimScan = false;	/* ...don't call _bt_first again, though */
+	so->needPrimScan = false;	/* ...and don't call _bt_first again */
 
 	/* Caller's tuple doesn't match any qual */
 	return false;
@@ -1634,6 +1645,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
+	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
 							arrayKeys, pstate->prechecked, pstate->firstmatch,
@@ -1688,62 +1700,36 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
+		/* Override _bt_check_compare, continue primitive scan */
+		pstate->continuescan = true;
+
 		/*
-		 * Tuple is still before the start of matches according to the scan's
-		 * required array keys (according to _all_ of its required equality
-		 * strategy keys, actually).
+		 * We will end up here repeatedly given a group of tuples > the
+		 * previous array keys and < the now-current keys (for a backwards
+		 * scan it's just the same, though the operators swap positions).
 		 *
-		 * _bt_advance_array_keys occasionally sets so->scanBehind to signal
-		 * that the scan's current position/tuples might be significantly
-		 * behind (multiple pages behind) its current array keys.  When this
-		 * happens, we need to be prepared to recover by starting a new
-		 * primitive index scan here, on our own.
+		 * We must avoid allowing this linear search process to scan very many
+		 * tuples from well before the start of tuples matching the current
+		 * array keys (or from well before the point where we'll once again
+		 * have to advance the scan's array keys).
+		 *
+		 * We keep the overhead under control by speculatively "looking ahead"
+		 * to later still-unscanned items from this same leaf page.  We'll
+		 * only attempt this once the number of tuples that the linear search
+		 * process has examined starts to get out of hand.
 		 */
-		Assert(!so->scanBehind ||
-			   so->keyData[ikey].sk_strategy == BTEqualStrategyNumber);
-		if (unlikely(so->scanBehind) && pstate->finaltup &&
-			_bt_tuple_before_array_skeys(scan, dir, pstate->finaltup, tupdesc,
-										 BTreeTupleGetNAtts(pstate->finaltup,
-															scan->indexRelation),
-										 false, 0, NULL))
+		pstate->rechecks++;
+		if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
 		{
-			/* Cut our losses -- start a new primitive index scan now */
-			pstate->continuescan = false;
-			so->needPrimScan = true;
-		}
-		else
-		{
-			/* Override _bt_check_compare, continue primitive scan */
-			pstate->continuescan = true;
+			/* See if we should skip ahead within the current leaf page */
+			_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
 
 			/*
-			 * We will end up here repeatedly given a group of tuples > the
-			 * previous array keys and < the now-current keys (for a backwards
-			 * scan it's just the same, though the operators swap positions).
-			 *
-			 * We must avoid allowing this linear search process to scan very
-			 * many tuples from well before the start of tuples matching the
-			 * current array keys (or from well before the point where we'll
-			 * once again have to advance the scan's array keys).
-			 *
-			 * We keep the overhead under control by speculatively "looking
-			 * ahead" to later still-unscanned items from this same leaf page.
-			 * We'll only attempt this once the number of tuples that the
-			 * linear search process has examined starts to get out of hand.
+			 * Might have set pstate.skip to a later page offset.  When that
+			 * happens then _bt_readpage caller will inexpensively skip ahead
+			 * to a later tuple from the same page (the one just after the
+			 * tuple we successfully "looked ahead" to).
 			 */
-			pstate->rechecks++;
-			if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
-			{
-				/* See if we should skip ahead within the current leaf page */
-				_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
-
-				/*
-				 * Might have set pstate.skip to a later page offset.  When
-				 * that happens then _bt_readpage caller will inexpensively
-				 * skip ahead to a later tuple from the same page (the one
-				 * just after the tuple we successfully "looked ahead" to).
-				 */
-			}
 		}
 
 		/* This indextuple doesn't match the current qual, in any case */
@@ -1760,6 +1746,39 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 								  ikey, true);
 }
 
+/*
+ * Test whether finaltup (the final tuple on the page) is still before the
+ * start of matches for the current array keys.
+ *
+ * Caller's finaltup tuple is the page high key (for forwards scans), or the
+ * first non-pivot tuple (for backwards scans).  Called during scans with
+ * array keys when the so->scanBehind flag was set on the previous page.
+ *
+ * Returns false if the tuple is still before the start of matches.  When that
+ * happens caller should cut its losses and start a new primitive index scan.
+ * Otherwise returns true.
+ */
+bool
+_bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
+						 IndexTuple finaltup)
+{
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	int			nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+
+	Assert(so->numArrayKeys);
+
+	if (_bt_tuple_before_array_skeys(scan, dir, finaltup, tupdesc,
+									 nfinaltupatts, false, 0, NULL))
+		return false;
+
+	if (!so->oppositeDirCheck)
+		return true;
+
+	return _bt_oppodir_checkkeys(scan, dir, finaltup);
+}
+
 /*
  * Test whether an indextuple fails to satisfy an inequality required in the
  * opposite direction only.
@@ -1778,7 +1797,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
  * _bt_checkkeys to stop the scan to consider array advancement/starting a new
  * primitive index scan.
  */
-bool
+static bool
 _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 					  IndexTuple finaltup)
 {
-- 
2.47.2

In reply to: Peter Geoghegan (#66)
7 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Thu, Feb 27, 2025 at 1:23 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v26, which has no functional changes. This is just to fix
yet more bitrot.

Attached is v27, which fixes bitrot. It also has some notable changes:

* New approach to "Index Searches: N" instrumentation patch, following
the recent revert of the original approach following
"debug_parallel_query=regress" buildfarm failures.

This is being discussed over on the thread that I started to discuss
the EXPLAIN ANALYZE instrumentation work:

/messages/by-id/CAH2-Wzk+cXBD1tnhQ-oagHuY9Fw5uArJE+LxfAP2VjZmDawbeQ@mail.gmail.com

* New patch that makes BTMaxItemSize not require a "Page" arg, so that
it can be used in contexts where a page image isn't close at hand.
This is preparation for the approach taken to parallel index scans,
where we apply a conservative "1/3 of a page" worst case limit on the
size of a datum, but don't have a page to pass to BTMaxItemSize.

I plan on committing this one soon. It's obviously pretty pointless to
make the BTMaxItemSize operate off of a page header, and not requiring
it is more flexible.

--
Peter Geoghegan

Attachments:

v27-0007-DEBUG-Add-skip-scan-disable-GUCs.patchapplication/x-patch; name=v27-0007-DEBUG-Add-skip-scan-disable-GUCs.patchDownload
From 7cf03af353f7ca4f5f2143f4ff83a2475e3927de Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 18 Jan 2025 10:54:44 -0500
Subject: [PATCH v27 7/7] DEBUG: Add skip scan disable GUCs.

---
 src/include/access/nbtree.h                   |  4 +++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 31 +++++++++++++++++++
 src/backend/utils/misc/guc_tables.c           | 23 ++++++++++++++
 3 files changed, 58 insertions(+)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index f25f4b05d..2977acd4a 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1184,6 +1184,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index c48c4f6c1..131e227e5 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -21,6 +21,27 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 typedef struct BTScanKeyPreproc
 {
 	ScanKey		inkey;
@@ -1644,6 +1665,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
 			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
 
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].sksup = NULL;
+
 			/*
 			 * We'll need a 3-way ORDER proc.  Set that up now.
 			 */
@@ -2148,6 +2173,12 @@ _bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
 		if (attno_has_rowcompare)
 			break;
 
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
 		/*
 		 * Now consider next attno_inkey (or keep going if this is an
 		 * additional scan key against the same attribute)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index ad25cbb39..7e4fa5c31 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1784,6 +1785,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3661,6 +3673,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
-- 
2.47.2

v27-0006-Apply-low-order-skip-key-in-_bt_first-more-often.patchapplication/x-patch; name=v27-0006-Apply-low-order-skip-key-in-_bt_first-more-often.patchDownload
From fd773c2a8f1028cfb9132870a0773c4043a20f15 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 13 Jan 2025 16:08:32 -0500
Subject: [PATCH v27 6/7] Apply low-order skip key in _bt_first more often.

Convert low_compare and high_compare nbtree skip array inequalities
(with opclasses that offer skip support) such that _bt_first is
consistently able to use later keys when descending the tree within
_bt_first.

For example, an index qual "WHERE a > 5 AND b = 2" is now converted to
"WHERE a >= 6 AND b = 2" by a new preprocessing step that takes place
after a final low_compare and/or high_compare are chosen by all earlier
preprocessing steps.  That way the scan's initial call to _bt_first will
use "WHERE a >= 6 AND b = 2" to find the initial leaf level position,
rather than merely using "WHERE a > 5" -- "b = 2" can always be applied.
This has a decent chance of making the scan avoid an extra _bt_first
call that would otherwise be needed just to determine the lowest-sorting
"a" value in the index (the lowest that still satisfies "WHERE a > 5").

The transformation process can only lower the total number of index
pages read when the use of a more restrictive set of initial positioning
keys in _bt_first actually allows the scan to land on some later leaf
page directly, relative to the unoptimized case (or on an earlier leaf
page directly, when scanning backwards).  The savings can be far greater
when affected skip arrays come after some higher-order array.  For
example, a qual "WHERE x IN (1, 2, 3) AND y > 5 AND z = 2" can now save
as many as 3 _bt_first calls as a result of these transformations (there
can be as many as 1 _bt_first call saved per "x" array element).

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=FJ78K3WsF3iWNxWnUCY9f=Jdg3QPxaXE=uYUbmuRz5Q@mail.gmail.com
---
 src/backend/access/nbtree/nbtpreprocesskeys.c | 181 ++++++++++++++++++
 src/test/regress/expected/create_index.out    |  21 ++
 src/test/regress/sql/create_index.sql         |  10 +
 3 files changed, 212 insertions(+)

diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index bd993b478..c48c4f6c1 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -50,6 +50,12 @@ static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
 								 BTArrayKeyInfo *array, bool *qual_ok);
 static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
 								 BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+									   BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
@@ -1290,6 +1296,172 @@ _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
 	return true;
 }
 
+/*
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts their
+ * operator strategies, while changing the operator as appropriate).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value MINVAL, using a transformed >= key
+ * instead of using the original > key makes it safe to include lower-order
+ * scan keys in the insertion scan key (there must be lower-order scan keys
+ * after the skip array).  We will avoid an extra _bt_first to find the first
+ * value in the index > sk_argument -- at least when the first real matching
+ * value in the index happens to be an exact match for the sk_argument value
+ * that we produced here by incrementing the original input key's sk_argument.
+ * (Backwards scans derive the same benefit when they encounter the sentinel
+ * value MAXVAL, by converting the high_compare key from < to <=.)
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01'.
+ *
+ * Note: We can safely modify array->low_compare/array->high_compare in place
+ * because they just point to copies of our scan->keyData[] input scan keys
+ * (namely the copies returned by _bt_preprocess_array_keys to be used as
+ * input into the standard preprocessing steps in _bt_preprocess_keys).
+ * Everything will be reset if there's a rescan.
+ */
+static void
+_bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+						   BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	/*
+	 * Called last among all preprocessing steps, when the skip array's final
+	 * low_compare and high_compare have both been chosen
+	 */
+	Assert(so->skipScan);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1 && !array->null_elem && array->sksup);
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skiparray_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skiparray_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1825,6 +1997,15 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && array->sksup &&
+						!array->null_elem)
+						_bt_skiparray_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 15dde752f..fa4517e2d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2589,6 +2589,27 @@ ORDER BY thousand;
         1 |     1001
 (1 row)
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+                                            QUERY PLAN                                            
+--------------------------------------------------------------------------------------------------
+ Limit
+   ->  Index Only Scan using tenk1_thous_tenthous on tenk1
+         Index Cond: ((thousand > '-1'::integer) AND (tenthous = ANY ('{1001,3000}'::integer[])))
+(3 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+        1 |     1001
+(2 rows)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index 40ba3d65b..e8c16959a 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -993,6 +993,16 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
 ORDER BY thousand;
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
-- 
2.47.2

v27-0004-Add-nbtree-skip-scan-optimizations.patchapplication/x-patch; name=v27-0004-Add-nbtree-skip-scan-optimizations.patchDownload
From 5bf2c1684305b0f3ee5b144ae537defc282113e9 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v27 4/7] Add nbtree skip scan optimizations.

Teach nbtree composite index scans to opportunistically skip over
irrelevant sections of composite indexes given a query with an omitted
prefix column.  When nbtree is passed input scan keys derived from a
query predicate "WHERE b = 5", new nbtree preprocessing steps now output
"WHERE a = ANY(<every possible 'a' value>) AND b = 5" scan keys.  That
is, preprocessing generates a "skip array" (along with an associated
scan key) for the omitted column "a", which makes it safe to mark the
scan key on "b" as required to continue the scan.  This is far more
efficient than a traditional full index scan whenever it allows the scan
to skip over many irrelevant leaf pages, by iteratively repositioning
itself using the keys on "a" and "b" together.

A skip array has "elements" that are generated procedurally and on
demand, but otherwise works just like a regular ScalarArrayOp array.
Preprocessing can freely add a skip array before or after any input
ScalarArrayOp arrays.  Index scans with a skip array decide when and
where to reposition the scan using the same approach as any other scan
with array keys.  This design builds on the design for array advancement
and primitive scan scheduling added to Postgres 17 by commit 5bf748b8.

The core B-Tree operator classes on most discrete types generate their
array elements with the help of their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the next
possible indexable value frequently turns out to be the next value
stored in the index.  Opclasses that lack a skip support routine fall
back on having nbtree "increment" (or "decrement") a skip array's
current element by setting the NEXT (or PRIOR) scan key flag, without
directly changing the scan key's sk_argument.  These sentinel values
behave just like any other value from an array -- though they can never
locate equal index tuples (they can only locate the next group of index
tuples containing the next set of non-sentinel values that the scan's
arrays need to advance to).

Inequality scan keys can affect how skip arrays generate their values.
Their range is constrained by the inequalities.  For example, a skip
array on "a" will only use element values 1 and 2 given a qual such as
"WHERE a BETWEEN 1 AND 2 AND b = 66".  A scan using such a skip array
has almost identical performance characteristics to one with the qual
"WHERE a = ANY('{1, 2}') AND b = 66".  The scan will be much faster when
it can be executed as two selective primitive index scans instead of a
single very large index scan that reads many irrelevant leaf pages.
However, the array transformation process won't always lead to improved
performance at runtime.  Much depends on physical index characteristics.

B-Tree preprocessing is optimistic about skipping working out: it
applies static, generic rules when determining where to generate skip
arrays, which assumes that the runtime overhead of maintaining skip
arrays will pay for itself -- or lead to only a modest performance loss.
As things stand, these assumptions are much too optimistic: skip array
maintenance will lead to unacceptable regressions with unsympathetic
queries (queries whose scan has little to no chance of avoid reading
irrelevant leaf pages).  An upcoming commit will address the problems in
this area by adding a mechanism that greatly lowers the cost of array
maintenance in these unfavorable cases.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |   3 +-
 src/include/access/nbtree.h                   |  35 +-
 src/include/catalog/pg_amproc.dat             |  22 +
 src/include/catalog/pg_proc.dat               |  27 +
 src/include/utils/skipsupport.h               |  98 +++
 src/backend/access/index/indexam.c            |   3 +-
 src/backend/access/nbtree/nbtcompare.c        | 273 +++++++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 600 ++++++++++++--
 src/backend/access/nbtree/nbtree.c            | 187 ++++-
 src/backend/access/nbtree/nbtsearch.c         | 111 ++-
 src/backend/access/nbtree/nbtutils.c          | 754 ++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |   4 +
 src/backend/commands/opclasscmds.c            |  25 +
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/date.c                  |  46 ++
 src/backend/utils/adt/meson.build             |   1 +
 src/backend/utils/adt/selfuncs.c              | 450 ++++++++---
 src/backend/utils/adt/skipsupport.c           |  61 ++
 src/backend/utils/adt/timestamp.c             |  48 ++
 src/backend/utils/adt/uuid.c                  |  70 ++
 doc/src/sgml/btree.sgml                       |  34 +-
 doc/src/sgml/indexam.sgml                     |   3 +-
 doc/src/sgml/indices.sgml                     |  49 +-
 doc/src/sgml/monitoring.sgml                  |   4 +-
 doc/src/sgml/xindex.sgml                      |  16 +-
 src/test/regress/expected/alter_generic.out   |  10 +-
 src/test/regress/expected/btree_index.out     |  41 +
 src/test/regress/expected/create_index.out    | 183 ++++-
 src/test/regress/expected/psql.out            |   3 +-
 src/test/regress/sql/alter_generic.sql        |   5 +-
 src/test/regress/sql/btree_index.sql          |  21 +
 src/test/regress/sql/create_index.sql         |  63 +-
 src/tools/pgindent/typedefs.list              |   3 +
 33 files changed, 2891 insertions(+), 363 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c4a073773..52916bab7 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -214,7 +214,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index c9bc82eba..7b766da20 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -707,6 +708,10 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
  *	(BTOPTIONS_PROC).  These procedures define a set of user-visible
  *	parameters that can be used to control operator class behavior.  None of
  *	the built-in B-Tree operator classes currently register an "options" proc.
+ *
+ *	To facilitate more efficient B-Tree skip scans, an operator class may
+ *	choose to offer a sixth amproc procedure (BTSKIPSUPPORT_PROC).  For full
+ *	details, see src/include/utils/skipsupport.h.
  */
 
 #define BTORDER_PROC		1
@@ -714,7 +719,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1027,10 +1033,21 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
-	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+	int			cur_elem;		/* index of current element in elem_values */
+
+	/* fields for skip arrays, which generate element datums procedurally */
+	int16		attlen;			/* attr len in bytes */
+	bool		attbyval;		/* as FormData_pg_attribute.attbyval */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupport sksup;			/* skip support (NULL if opclass lacks it) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1042,6 +1059,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* arrays might be ahead of scan's key space? */
 	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
@@ -1119,6 +1137,15 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on column without input = */
+
+/* SK_BT_SKIP-only flags (mutable, set and unset by array advancement) */
+#define SK_BT_MINVAL	0x00080000	/* invalid sk_argument, use low_compare */
+#define SK_BT_MAXVAL	0x00100000	/* invalid sk_argument, use high_compare */
+#define SK_BT_NEXT		0x00200000	/* positions the scan > sk_argument */
+#define SK_BT_PRIOR		0x00400000	/* positions the scan < sk_argument */
+
+/* Remaps pg_index flag bits to uppermost SK_BT_* byte */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1165,7 +1192,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 19100482b..37000639f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cede992b6..dd9927aaf 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2285,6 +2300,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4475,6 +4493,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6347,6 +6368,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9395,6 +9419,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..bc51847cf
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining groups of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * It usually isn't feasible to implement skip support for an opclass whose
+ * input type is continuous.  The B-Tree code falls back on next-key sentinel
+ * values for any opclass that doesn't provide its own skip support function.
+ * This isn't really an implementation restriction; there is no benefit to
+ * providing skip support for an opclass where guessing that the next indexed
+ * value is the next possible indexable value never (or hardly ever) works out.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order)
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 * Operator class decrement/increment functions will never be called with
+	 * a NULL "existing" argument, either.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern SkipSupport PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+												 bool reverse);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 073d58bd2..e07f0bb4b 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -480,7 +480,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nscanbytes = add_size(nscanbytes,
-							  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+							  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																			  nkeys,
 																			  norderbys));
 	if (!instrument || nworkers == 0)
 	{
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 291cb8fc1..4da5a3c1d 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 38a87af1c..bd993b478 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -45,8 +45,15 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
+static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
+								 ScanKey skey, FmgrInfo *orderproc,
+								 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
+								 BTArrayKeyInfo *array, bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
+							   int *numSkipArrayKeys);
 static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
 									  Oid elemtype, StrategyNumber strat,
 									  Datum *elems, int nelems);
@@ -89,6 +96,8 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -101,10 +110,16 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never generated skip array scan keys, it would be possible for "gaps"
+ * to appear that make it unsafe to mark any subsequent input scan keys
+ * (copied from scan->keyData[]) as required to continue the scan.  Prior to
+ * Postgres 18, a qual like "WHERE y = 4" always required a full index scan.
+ * It'll now become "WHERE x = ANY('{every possible x value}') and y = 4" on
+ * output, due to the addition of a skip array on "x".  This has the potential
+ * to be much more efficient than a full index scan (though it'll behave like
+ * a full index scan when there are many distinct values for "x").
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -141,7 +156,12 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -200,6 +220,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProcs[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -231,6 +259,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
+		Assert(!so->skipScan);
 
 		return;
 	}
@@ -288,7 +317,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -345,7 +375,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -393,6 +422,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, _bt_preprocess_array_keys has already added "=" keys
+			 * sufficient to form an unbroken series of "=" constraints on all
+			 * attrs prior to the attr from the final scan->keyData[] key.
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -481,6 +515,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -489,6 +524,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -508,8 +544,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -803,6 +837,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -812,6 +849,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -885,6 +938,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -978,24 +1032,55 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
  * Compare an array scan key to a scalar scan key, eliminating contradictory
  * array elements such that the scalar scan key becomes redundant.
  *
+ * If the opfamily is incomplete we may not be able to determine which
+ * elements are contradictory.  When we return true we'll have validly set
+ * *qual_ok, guaranteeing that at least the scalar scan key can be considered
+ * redundant.  We return false if the comparison could not be made (caller
+ * must keep both scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar scan keys.
+ */
+static bool
+_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
+							   bool *qual_ok)
+{
+	Assert(arraysk->sk_attno == skey->sk_attno);
+	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
+		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
+	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
+		   skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Just call the appropriate helper function based on whether it's a SAOP
+	 * array or a skip array.  Both helpers will set *qual_ok in passing.
+	 */
+	if (array->num_elems != -1)
+		return _bt_saoparray_shrink(scan, arraysk, skey, orderproc, array,
+									qual_ok);
+	else
+		return _bt_skiparray_shrink(scan, skey, array, qual_ok);
+}
+
+/*
+ * Preprocessing of SAOP (non-skip) array scan key, used to determine which
+ * array elements are eliminated as contradictory by a non-array scalar key.
+ * _bt_compare_array_scankey_args helper function.
+ *
  * Array elements can be eliminated as contradictory when excluded by some
  * other operator on the same attribute.  For example, with an index scan qual
  * "WHERE a IN (1, 2, 3) AND a < 2", all array elements except the value "1"
  * are eliminated, and the < scan key is eliminated as redundant.  Cases where
  * every array element is eliminated by a redundant scalar scan key have an
  * unsatisfiable qual, which we handle by setting *qual_ok=false for caller.
- *
- * If the opfamily doesn't supply a complete set of cross-type ORDER procs we
- * may not be able to determine which elements are contradictory.  If we have
- * the required ORDER proc then we return true (and validly set *qual_ok),
- * guaranteeing that at least the scalar scan key can be considered redundant.
- * We return false if the comparison could not be made (caller must keep both
- * scan keys when this happens).
  */
 static bool
-_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
-							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
-							   bool *qual_ok)
+_bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+					 FmgrInfo *orderproc, BTArrayKeyInfo *array, bool *qual_ok)
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
@@ -1006,14 +1091,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
-	Assert(arraysk->sk_attno == skey->sk_attno);
 	Assert(array->num_elems > 0);
-	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
-		   arraysk->sk_strategy == BTEqualStrategyNumber);
-	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
-		   skey->sk_strategy != BTEqualStrategyNumber);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
 
 	/*
 	 * _bt_binsrch_array_skey searches an array for the entry best matching a
@@ -1112,6 +1191,105 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	return true;
 }
 
+/*
+ * Preprocessing of skip (non-SAOP) array scan key, used to determine
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function.
+ *
+ * Unlike _bt_saoparray_shrink, we don't modify caller's array in-place.  Skip
+ * arrays work by procedurally generating their elements as needed, so we just
+ * store the inequality as the skip array's low_compare or high_compare.  The
+ * array's elements will be generated from the range of values that satisfies
+ * both low_compare and high_compare.
+ */
+static bool
+_bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
+					 bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Array's index attribute will be constrained by a strict operator/key.
+	 * Array must not "contain a NULL element" (i.e. the scan must not apply
+	 * "IS NULL" qual when it reaches the end of the index that stores NULLs).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Consider if we should treat caller's scalar scan key as the skip
+	 * array's high_compare or low_compare.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way the scan can always safely assume that
+	 * it's okay to use the input-opclass-only-type proc from so->orderProcs[]
+	 * (they can be cross-type with SAOP arrays, but never with skip arrays).
+	 *
+	 * This approach is enabled by MINVAL/MAXVAL sentinel key markings, which
+	 * can be thought of as representing either the lowest or highest matching
+	 * array element (excluding the NULL element, where applicable, though as
+	 * just discussed it isn't applicable to this range skip array anyway).
+	 * Array keys marked MINVAL/MAXVAL never have a valid datum in their
+	 * sk_argument field.  The scan directly applies the array's low_compare
+	 * key when it encounters MINVAL in the array key proper (just as it
+	 * applies high_compare when it sees MAXVAL set in the array key proper).
+	 * The scan must never use the array's so->orderProcs[] proc against
+	 * low_compare's/high_compare's sk_argument, either (so->orderProcs[] is
+	 * only intended to be used with rhs datums from the array proper/index).
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* replace existing high_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing high_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's high_compare */
+			array->high_compare = skey;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* replace existing low_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing low_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's low_compare */
+			array->low_compare = skey;
+			break;
+		case BTEqualStrategyNumber:
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1137,6 +1315,12 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -1152,41 +1336,36 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	Oid			skip_eq_ops[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
-	ScanKey		cur;
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
+	so->skipScan = (numSkipArrayKeys > 0);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys we'll need to generate below
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -1201,18 +1380,20 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
+		ScanKey		inkey = scan->keyData + input_ikey,
+					cur;
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
 		Oid			elemtype;
@@ -1225,21 +1406,113 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		Datum	   *elem_values;
 		bool	   *elem_nulls;
 		int			num_nonnulls;
-		int			j;
+
+		/* set up next output scan key */
+		cur = &arrayKeyData[numArrayKeyData];
+
+		/* Backfill skip arrays for attrs < or <= input key's attr? */
+		while (numSkipArrayKeys && attno_skip <= inkey->sk_attno)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skip_eq_ops[attno_skip - 1];
+			CompactAttribute *attr;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/*
+				 * Attribute already has an = input key, so don't output a
+				 * skip array for attno_skip.  Just copy attribute's = input
+				 * key into arrayKeyData[] once outside this inner loop.
+				 *
+				 * Note: When we get here there must be a later attribute that
+				 * lacks an equality input key, and still needs a skip array
+				 * (if there wasn't then numSkipArrayKeys would be 0 by now).
+				 */
+				Assert(attno_skip == inkey->sk_attno);
+				/* inkey can't be last input key to be marked required: */
+				Assert(input_ikey < scan->numberOfKeys - 1);
+#if 0
+				/* Could be a redundant input scan key, so can't do this: */
+				Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+					   (inkey->sk_flags & SK_SEARCHNULL));
+#endif
+
+				attno_skip++;
+				break;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize generic BTArrayKeyInfo fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+
+			/* Initialize skip array specific BTArrayKeyInfo fields */
+			attr = TupleDescCompactAttr(RelationGetDescr(rel), attno_skip - 1);
+			reverse = (indoption[attno_skip - 1] & INDOPTION_DESC) != 0;
+			so->arrayKeys[numArrayKeys].attlen = attr->attlen;
+			so->arrayKeys[numArrayKeys].attbyval = attr->attbyval;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup =
+				PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse);
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * We'll need a 3-way ORDER proc.  Set that up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			numArrayKeys++;
+			numArrayKeyData++;	/* keep this scan key/array */
+
+			/* set up next output scan key */
+			cur = &arrayKeyData[numArrayKeyData];
+
+			/* remember having output this skip array and scan key */
+			numSkipArrayKeys--;
+			attno_skip++;
+		}
 
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
-		*cur = scan->keyData[input_ikey];
+		*cur = *inkey;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -1257,7 +1530,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * all btree operators are strict.
 		 */
 		num_nonnulls = 0;
-		for (j = 0; j < num_elems; j++)
+		for (int j = 0; j < num_elems; j++)
 		{
 			if (!elem_nulls[j])
 				elem_values[num_nonnulls++] = elem_values[j];
@@ -1295,7 +1568,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -1306,7 +1579,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -1323,7 +1596,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -1392,23 +1665,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			origelemtype = elemtype;
 		}
 
-		/*
-		 * And set up the BTArrayKeyInfo data.
-		 *
-		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
-		 * scan_key field later on, after so->keyData[] has been finalized.
-		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		/* Initialize generic BTArrayKeyInfo fields */
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
+
+		/* Initialize SAOP array specific BTArrayKeyInfo fields */
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].cur_elem = -1;	/* i.e. invalid */
+
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
+	Assert(numSkipArrayKeys == 0);
+
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -1514,7 +1788,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -1575,6 +1850,189 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit every so->arrayKeys[] entry.
+ *
+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller must add this many
+ * skip arrays to each of the most significant attributes lacking any keys
+ * that use the = strategy (IS NULL keys count as = keys here).  The specific
+ * attributes that need skip arrays are indicated by initializing caller's
+ * skip_eq_ops[] 0-based attribute offset to a valid = op strategy Oid.  We'll
+ * only ever set skip_eq_ops[] entries to InvalidOid for attributes that
+ * already have an equality key in scan->keyData[] input keys -- and only when
+ * there's some later "attribute gap" for us to "fill-in" with a skip array.
+ *
+ * We're optimistic about skipping working out: we always add exactly the skip
+ * arrays needed to maximize the number of input scan keys that can ultimately
+ * be marked as required to continue the scan (but no more).  For a composite
+ * index on (a, b, c, d), we'll instruct caller to add skip arrays as follows:
+ *
+ * Input keys						Output keys (after all preprocessing)
+ * ----------						-------------------------------------
+ * a = 1							a = 1 (no skip arrays)
+ * a = ANY('{0, 1}')				a = ANY('{0, 1}') (no skip arrays)
+ * b = 42							skip a AND b = 42
+ * b = ANY('{40, 42}')				skip a AND b = ANY('{40, 42}')
+ * a = 1 AND b = 42					a = 1 AND b = 42 (no skip arrays)
+ * a >= 1 AND b = 42				range skip a AND b = 42
+ * a = 1 AND b > 42					a = 1 AND b > 42 (no skip arrays)
+ * a >= 1 AND a <= 3 AND b = 42		range skip a AND b = 42
+ * a = 1 AND c <= 27				a = 1 AND skip b AND c <= 27
+ * a = 1 AND d >= 1					a = 1 AND skip b AND skip c AND d >= 1
+ * a = 1 AND b >= 42 AND d > 1		a = 1 AND range skip b AND skip c AND d > 1
+ */
+static int
+_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_skip = 1,
+				attno_inkey = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+#ifdef DEBUG_DISABLE_SKIP_SCAN
+	/* don't attempt to add skip arrays */
+	return numArrayKeys;
+#endif
+
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+			/* Look up input opclass's equality operator (might fail) */
+			skip_eq_ops[attno_skip - 1] =
+				get_opfamily_member(opfamily, opcintype, opcintype,
+									BTEqualStrategyNumber);
+			if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key
+		 * (doesn't matter whether that attribute has an equality key, or just
+		 * uses inequality keys).
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys
+			 */
+			if (attno_has_equal)
+			{
+				/* Attributes with an = key must have InvalidOid eq_op set */
+				skip_eq_ops[attno_skip - 1] = InvalidOid;
+			}
+			else
+			{
+				Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+				Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+				/* Look up input opclass's equality operator (might fail) */
+				skip_eq_ops[attno_skip - 1] =
+					get_opfamily_member(opfamily, opcintype, opcintype,
+										BTEqualStrategyNumber);
+
+				if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+				{
+					/*
+					 * Input opclass lacks an equality strategy operator, so
+					 * don't generate a skip array that definitely won't work
+					 */
+					break;
+				}
+
+				/* plan on adding a backfill skip array for this attribute */
+				(*numSkipArrayKeys)++;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required later on.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
 /*
  * _bt_find_extreme_element() -- get least or greatest array element
  *
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index c0a8833e0..912062c1f 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -30,6 +30,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -78,11 +79,24 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 *
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -335,6 +349,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	else
 		so->keyData = NULL;
 
+	so->skipScan = false;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->oppositeDirCheck = false;
@@ -540,10 +555,158 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume that every input scankey will be output with its
+	 * own SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * array (and an associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		CompactAttribute *attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescCompactAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to space for the largest possible
+		 * index tuple.  This is quite conservative, especially when there are
+		 * multiple varlena columns.  (We could use less memory here, but it
+		 * seems risky to rely on the implementation details that would make
+		 * it safe from this distance.)
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BTMaxItemSize);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   array->attbyval, array->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array key, while freeing memory allocated for old
+		 * sk_argument where required
+		 */
+		if (!array->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -612,6 +775,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -678,14 +842,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -830,6 +989,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -848,12 +1008,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
 	LWLockRelease(&btscan->btps_lock);
 }
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 3fb0a0380..27abb7ef9 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -965,6 +965,15 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  All
+	 * array keys are always considered = keys, but we'll sometimes need to
+	 * treat the current key value as if we were using an inequality strategy.
+	 * This happens whenever a skip array's scan key is found to have been set
+	 * to one of the special sentinel values representing an imaginary point
+	 * before/after some (or all) of the index's real indexable values.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1040,8 +1049,56 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is MINVAL, choose low_compare (when scanning
+				 * backwards it'll be MAXVAL, and we'll choose high_compare).
+				 *
+				 * Note: if the array's low_compare key makes 'chosen' NULL,
+				 * then we behave as if the array's first element is -inf,
+				 * except when !array->null_elem implies a usable NOT NULL
+				 * constraint.
+				 */
+				if (chosen != NULL &&
+					(chosen->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skipequalitykey = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					Assert(so->skipScan);
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MAXVAL));
+						chosen = array->low_compare;
+					}
+					else
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MINVAL));
+						chosen = array->high_compare;
+					}
+
+					Assert(chosen == NULL ||
+						   chosen->sk_attno == skipequalitykey->sk_attno);
+
+					if (!array->null_elem)
+						impliesNN = skipequalitykey;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1084,9 +1141,41 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 
 				/*
-				 * Done if that was the last attribute, or if next key is not
-				 * in sequence (implying no boundary key is available for the
-				 * next attribute).
+				 * If the key that we just added to startKeys[] is a skip
+				 * array = key whose current element is marked NEXT or PRIOR,
+				 * make strat_total > or < (and stop adding boundary keys).
+				 * This can only happen with opclasses that lack skip support.
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(so->skipScan);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(strat_total == BTEqualStrategyNumber);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We're done.  We'll never find an exact = match for a
+					 * NEXT or PRIOR sentinel sk_argument value.  There's no
+					 * sense in trying to add more keys to startKeys[].
+					 */
+					break;
+				}
+
+				/*
+				 * Done if that was the last scan key output by preprocessing.
+				 * Also done if there is a gap index attribute that lacks a
+				 * usable key (only possible when preprocessing was unable to
+				 * generate a skip array key to "fill in the gap").
 				 */
 				if (i >= so->numberOfKeys ||
 					cur->sk_attno != curattr + 1)
@@ -1578,10 +1667,12 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * corresponding value from the last item on the page.  So checking with
 	 * the last item on the page would give a more precise answer.
 	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
+	 * We don't do this for the first page read by each (primitive) scan, to
+	 * avoid slowing down point queries.  They typically don't stand to gain
+	 * much when the optimization can be applied, and are more likely to
+	 * notice the overhead of the precheck.  Also avoid it during skip scans,
+	 * where individual primitive scans that don't just read one leaf page are
+	 * likely to advance their skip array(s) several times per leaf page read.
 	 *
 	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
 	 * just set a low-order required array's key to the best available match
@@ -1605,7 +1696,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !so->skipScan && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 4e455a66b..49afe779f 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -30,6 +30,17 @@
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									  int32 set_elem_result, Datum tupdatum, bool tupnull);
+static void _bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_array_set_low_or_high(Relation rel, ScanKey skey,
+									  BTArrayKeyInfo *array, bool low_not_high);
+static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -207,6 +218,7 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 	int32		result = 0;
 
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
+	Assert(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)));
 
 	if (tupnull)				/* NULL tupdatum */
 	{
@@ -283,6 +295,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* SAOP arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -405,6 +419,185 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, since skip arrays don't really
+ * contain elements (they generate their array elements procedurally instead).
+ * Our interface matches that of _bt_binsrch_array_skey in every other way.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  We return -1 when tupdatum/tupnull is lower that any value
+ * within the range of the array, and 1 when it is higher than every value.
+ * Caller should pass *set_elem_result to _bt_skiparray_set_element to advance
+ * the array.
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (array->null_elem)
+	{
+		Assert(!array->low_compare && !array->high_compare);
+
+		*set_elem_result = 0;
+		return;
+	}
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+
+	/*
+	 * Assert that any keys that were assumed to be satisfied already (due to
+	 * caller passing cur_elem_trig=true) really are satisfied as expected
+	 */
+#ifdef USE_ASSERT_CHECKING
+	if (cur_elem_trig)
+	{
+		if (ScanDirectionIsForward(dir) && array->low_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												  array->low_compare->sk_collation,
+												  tupdatum,
+												  array->low_compare->sk_argument)));
+
+		if (ScanDirectionIsBackward(dir) && array->high_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												  array->high_compare->sk_collation,
+												  tupdatum,
+												  array->high_compare->sk_argument)));
+	}
+#endif
+}
+
+/*
+ * _bt_skiparray_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Caller passes set_elem_result returned by _bt_binsrch_skiparray_skey for
+ * caller's tupdatum/tupnull.
+ *
+ * We copy tupdatum/tupnull into skey's sk_argument iff set_elem_result == 0.
+ * Otherwise, we set skey to either the lowest or highest value that's within
+ * the range of caller's skip array (whichever is the best available match to
+ * tupdatum/tupnull that is still within the range of the skip array according
+ * to _bt_binsrch_skiparray_skey/set_elem_result).
+ */
+static void
+_bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  int32 set_elem_result, Datum tupdatum, bool tupnull)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (set_elem_result)
+	{
+		/* tupdatum/tupnull is out of the range of the skip array */
+		Assert(!array->null_elem);
+
+		_bt_array_set_low_or_high(rel, skey, array, set_elem_result < 0);
+		return;
+	}
+
+	/* Advance skip array to tupdatum (or tupnull) value */
+	if (unlikely(tupnull))
+	{
+		_bt_skiparray_set_isnull(rel, skey, array);
+		return;
+	}
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen);
+}
+
+/*
+ * _bt_skiparray_set_isnull() -- set skip array scan key to NULL
+ */
+static void
+_bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->null_elem && !array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -414,29 +607,355 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_array_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_array_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  bool low_not_high)
+{
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting array to lowest element (according to low_compare) */
+		skey->sk_flags |= SK_BT_MINVAL;
+	}
+	else
+	{
+		/* Setting array to highest element (according to high_compare) */
+		skey->sk_flags |= SK_BT_MAXVAL;
+	}
+}
+
+/*
+ * _bt_array_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the minimum value within the range
+	 * of a skip array (often just -inf) is never decrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		/* Successfully "decremented" array */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly decrement sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Decrement" from NULL to the high_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->high_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup->decrement(rel, skey->sk_argument, &uflow);
+	if (unlikely(uflow))
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			_bt_skiparray_set_isnull(rel, skey, array);
+
+			/* Successfully "decremented" array to NULL */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	/* Successfully decremented array */
+	return true;
+}
+
+/*
+ * _bt_array_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MINVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the maximum value within the range
+	 * of a skip array (often just +inf) is never incrementable
+	 */
+	if (skey->sk_flags & SK_BT_MAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		/* Successfully "incremented" array */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly increment sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Increment" from NULL to the low_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->low_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup->increment(rel, skey->sk_argument, &oflow);
+	if (unlikely(oflow))
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			_bt_skiparray_set_isnull(rel, skey, array);
+
+			/* Successfully "incremented" array to NULL */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	/* Successfully incremented array */
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -452,6 +971,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -461,29 +981,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_array_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_array_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -507,7 +1028,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 }
 
 /*
- * _bt_rewind_nonrequired_arrays() -- Rewind non-required arrays
+ * _bt_rewind_nonrequired_arrays() -- Rewind SAOP arrays not marked required
  *
  * Called when _bt_advance_array_keys decides to start a new primitive index
  * scan on the basis of the current scan position being before the position
@@ -539,10 +1060,15 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
  *
  * Note: _bt_verify_arrays_bt_first is called by an assertion to enforce that
  * everybody got this right.
+ *
+ * Note: In practice almost all SAOP arrays are marked required during
+ * preprocessing (if necessary by generating skip arrays).  It is hardly ever
+ * truly necessary to call here, but consistently doing so is simpler.
  */
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -550,7 +1076,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -562,16 +1087,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_array_set_low_or_high(rel, cur, array,
+								  ScanDirectionIsForward(dir));
 	}
 }
 
@@ -696,9 +1215,77 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (likely(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))))
+		{
+			/* Scankey has a valid/comparable sk_argument value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0)
+			{
+				/*
+				 * Interpret result in a way that takes NEXT/PRIOR into
+				 * account
+				 */
+				if (cur->sk_flags & SK_BT_NEXT)
+					result = -1;
+				else if (cur->sk_flags & SK_BT_PRIOR)
+					result = 1;
+
+				Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP));
+			}
+		}
+		else
+		{
+			BTArrayKeyInfo *array = NULL;
+
+			/*
+			 * Current array element/array = scan key value is a sentinel
+			 * value that represents the lowest (or highest) possible value
+			 * that's still within the range of the array.
+			 *
+			 * Like _bt_first, we only see MINVAL keys during forwards scans
+			 * (and similarly only see MAXVAL keys during backwards scans).
+			 * Even if the scan's direction changes, we'll stop at some higher
+			 * order key before we can ever reach any MAXVAL (or MINVAL) keys.
+			 * (However, unlike _bt_first we _can_ get to keys marked either
+			 * NEXT or PRIOR, regardless of the scan's current direction.)
+			 */
+			Assert(ScanDirectionIsForward(dir) ?
+				   !(cur->sk_flags & SK_BT_MAXVAL) :
+				   !(cur->sk_flags & SK_BT_MINVAL));
+
+			/*
+			 * There are no valid sk_argument values in MINVAL/MAXVAL keys.
+			 * Check if tupdatum is within the range of skip array instead.
+			 */
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			_bt_binsrch_skiparray_skey(false, dir, tupdatum, tupnull,
+									   array, cur, &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum satisfies both low_compare and high_compare, so
+				 * it's time to advance the array keys.
+				 *
+				 * Note: It's possible that the skip array will "advance" from
+				 * its MINVAL (or MAXVAL) representation to an alternative,
+				 * logically equivalent representation of the same value: a
+				 * representation where the = key gets a valid datum in its
+				 * sk_argument.  This is only possible when low_compare uses
+				 * the >= strategy (or high_compare uses the <= strategy).
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1017,18 +1604,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1053,18 +1631,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -1080,14 +1649,22 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			bool		cur_elem_trig = (sktrig_required && ikey == sktrig);
 
 			/*
-			 * Binary search for closest match that's available from the array
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems == -1)
+				_bt_binsrch_skiparray_skey(cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * Binary search for the closest match from the SAOP array
+			 */
+			else
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 		}
 		else
 		{
@@ -1163,11 +1740,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+		if (array)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->num_elems == -1)
+			{
+				/* Skip array's new element is tupdatum (or MINVAL/MAXVAL) */
+				_bt_skiparray_set_element(rel, cur, array, result,
+										  tupdatum, tupnull);
+			}
+			else if (array->cur_elem != set_elem)
+			{
+				/* SAOP array's new element is set_elem datum */
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
 		}
 	}
 
@@ -1586,10 +2173,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -1920,6 +2508,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key uses one of several sentinel values.  We just
+		 * fall back on _bt_tuple_before_array_skeys when we see such a value.
+		 */
+		if (key->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL |
+							 SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index dd6f5a15c..817ad358f 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -106,6 +106,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 8546366ee..a6dd8eab5 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("ordering equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (GetIndexAmRoutineByAmId(amoid, false)->amcanhash)
 	{
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index f279853de..4227ab1a7 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f23cfad71..244f48f4f 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index b4dc91c7c..8911cd8ed 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -94,7 +94,6 @@
 
 #include "postgres.h"
 
-#include <ctype.h>
 #include <math.h>
 
 #include "access/brin.h"
@@ -193,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -214,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5768,6 +5771,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -6826,6 +6915,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -6835,17 +6971,22 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
+	double		correlation = 0;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
+	bool		set_correlation = false;
 	bool		eqQualHere;
-	bool		found_saop;
+	bool		found_array;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	bool		upper_inequal_col;
+	bool		lower_inequal_col;
 	double		num_sa_scans;
+	double		upperselectivity;
+	double		lowerselectivity;
 	ListCell   *lc;
 
 	/*
@@ -6856,21 +6997,33 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * it's OK to count them in indexSelectivity, but they should not count
 	 * for estimating numIndexTuples.  So we must examine the given indexquals
 	 * to find out which ones count as boundary quals.  We rely on the
-	 * knowledge that they are given in index column order.
+	 * knowledge that they are given in index column order (though this
+	 * process is complicated by the use of skip arrays, as explained below).
 	 *
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
+	 * If there's a ScalarArrayOp array in the quals, or if B-Tree
+	 * preprocessing will be able to generate a skip array, we'll actually
+	 * perform up to N index descents (not just one), but the underlying
 	 * operator can be considered to act the same as it normally does.
+	 *
+	 * In practice, non-leading quals often _can_ act as boundary quals due to
+	 * preprocessing generating a "bridging" skip array.  Whether or not we'll
+	 * actually treat lower-order quals as boundary quals (that is, quals that
+	 * influence our numIndexTuples estimate) is determined by heuristics.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
-	found_saop = false;
+	found_array = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
+	upper_inequal_col = false;
+	lower_inequal_col = false;
 	num_sa_scans = 1;
+	upperselectivity = 1.0;
+	lowerselectivity = 1.0;
 	foreach(lc, path->indexclauses)
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
@@ -6879,12 +7032,120 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		if (indexcol != iclause->indexcol)
 		{
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
-			eqQualHere = false;
-			indexcol++;
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't come after a RowCompare */
+
+			/*
+			 * Consider whether nbtree preprocessing will backfill skip arrays
+			 * for index columns lacking an equality clause, and account for
+			 * the cost of maintaining those skip arrays
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT,
+							new_num_sa_scans;
+				bool		isdefault = true;
+
+				found_array = true;
+
+				/*
+				 * Now estimate number of "array elements" using ndistinct.
+				 *
+				 * Internally, nbtree treats skip scans as scans with SAOP
+				 * style arrays that generate elements procedurally.  This is
+				 * like a "col = ANY('{every possible col value}')" qual.
+				 */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						Assert(!set_correlation);
+						correlation = btcost_correlation(index, &vardata);
+						set_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				/*
+				 * Apply the selectivities of any inequalities to ndistinct
+				 * iff there was a non-equality clause for this column and we
+				 * don't just have a default ndistinct estimate
+				 */
+				if ((upper_inequal_col || lower_inequal_col) && !isdefault)
+				{
+					double		ndistinctfrac = 1.0;
+
+					if (upper_inequal_col)
+						ndistinctfrac -= (1.0 - upperselectivity);
+					if (lower_inequal_col)
+						ndistinctfrac -= (1.0 - lowerselectivity);
+
+					CLAMP_PROBABILITY(ndistinctfrac);
+					ndistinct = rint(ndistinct * ndistinctfrac);
+					ndistinct = Max(ndistinct, 1);
+				}
+
+				/*
+				 * Account for possible +inf element, used to find the highest
+				 * item in the index when qual lacks a < or <= upper bound
+				 */
+				if (!upper_inequal_col)
+					ndistinct += 1;
+
+				/*
+				 * Account for possible -inf element, used to find the lowest
+				 * item in the index when qual lacks a > or >= lower bound
+				 */
+				if (!lower_inequal_col)
+					ndistinct += 1;
+
+				/* Forget about any upper_inequal_col/lower_inequal_col */
+				upper_inequal_col = false;
+				lower_inequal_col = false;
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * using skip scan.
+				 */
+				new_num_sa_scans = num_sa_scans * ndistinct;
+
+				/*
+				 * Stop adding new skip arrays when the would-be new
+				 * num_sa_scans exceeds the total number of index pages
+				 */
+				if (index->pages < new_num_sa_scans)
+				{
+					/* Qual (and later quals) won't affect numIndexTuples */
+					break;
+				}
+
+				/* Done counting skip array "elements" for this column */
+				num_sa_scans = new_num_sa_scans;
+				indexcol++;
+			}
+
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+				break;			/* no quals at all for indexcol (can't skip) */
+
+			/* reset for next indexcol */
+			eqQualHere = false;
+			upper_inequal_col = false;
+			lower_inequal_col = false;
+			upperselectivity = 1.0;
+			lowerselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6906,6 +7167,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -6914,7 +7176,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				double		alength = estimate_array_length(root, other_operand);
 
 				clause_op = saop->opno;
-				found_saop = true;
+				found_array = true;
 				/* estimate SA descents by indexBoundQuals only */
 				if (alength > 1)
 					num_sa_scans *= alength;
@@ -6926,7 +7188,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skip scan purposes */
 					eqQualHere = true;
 				}
 			}
@@ -6942,6 +7204,37 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+
+				if (!eqQualHere && !found_rowcompare &&
+					indexcol < index->nkeycolumns - 1)
+				{
+					double		selec;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct.  Set things
+					 * up now (will be used when we move onto the next clause
+					 * against some later index column)
+					 *
+					 * Like clauselist_selectivity, we recognize redundant
+					 * inequalities such as "x < 4 AND x < 5"; only the
+					 * tighter constraint will be counted.
+					 */
+					selec = (double) clause_selectivity(root, (Node *) rinfo,
+														0, JOIN_INNER, NULL);
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (selec < upperselectivity)
+							upperselectivity = selec;
+						upper_inequal_col = true;
+					}
+					else
+					{
+						if (selec < lowerselectivity)
+							lowerselectivity = selec;
+						lower_inequal_col = true;
+					}
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -6951,13 +7244,13 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	/*
 	 * If index is unique and we found an '=' clause for each column, we can
 	 * just assume numIndexTuples = 1 and skip the expensive
-	 * clauselist_selectivity calculations.  However, a ScalarArrayOp or
-	 * NullTest invalidates that theory, even though it sets eqQualHere.
+	 * clauselist_selectivity calculations.  However, an array or NullTest
+	 * invalidates that theory, even though it sets eqQualHere.
 	 */
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
-		!found_saop &&
+		!found_array &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
 	else
@@ -6979,11 +7272,11 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		numIndexTuples = btreeSelectivity * index->rel->tuples;
 
 		/*
-		 * btree automatically combines individual ScalarArrayOpExpr primitive
-		 * index scans whenever the tuples covered by the next set of array
-		 * keys are close to tuples covered by the current set.  That puts a
-		 * natural ceiling on the worst case number of descents -- there
-		 * cannot possibly be more than one descent per leaf page scanned.
+		 * btree automatically combines individual array primitive index scans
+		 * whenever the tuples covered by the next set of array keys are close
+		 * to tuples covered by the current set.  That puts a natural ceiling
+		 * on the worst case number of descents -- there cannot possibly be
+		 * more than one descent per leaf page scanned.
 		 *
 		 * Clamp the number of descents to at most 1/3 the number of index
 		 * pages.  This avoids implausibly high estimates with low selectivity
@@ -6997,16 +7290,18 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * of leaf pages (we make it 1/3 the total number of pages instead) to
 		 * give the btree code credit for its ability to continue on the leaf
 		 * level with low selectivity scans.
+		 *
+		 * Note: num_sa_scans includes both ScalarArrayOp array elements and
+		 * skip array elements whose qual affects our numIndexTuples estimate.
 		 */
 		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
-		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
-		 * to integer.
+		 * As in genericcostestimate(), we have to adjust for any array quals
+		 * included in indexBoundQuals, and then round to integer.
 		 *
-		 * It is tempting to make genericcostestimate behave as if SAOP
+		 * It is tempting to make genericcostestimate behave as if array
 		 * clauses work in almost the same way as scalar operators during
 		 * btree scans, making the top-level scan look like a continuous scan
 		 * (as opposed to num_sa_scans-many primitive index scans).  After
@@ -7059,110 +7354,25 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * cost is somewhat arbitrarily set at 50x cpu_operator_cost per page
 	 * touched.  The number of such pages is btree tree height plus one (ie,
 	 * we charge for the leaf page too).  As above, charge once per estimated
-	 * SA index descent.
+	 * index descent.
 	 */
 	descentCost = (index->tree_height + 1) * DEFAULT_PAGE_CPU_MULTIPLIER * cpu_operator_cost;
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!set_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..2bd35d2d2
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns skip support struct, allocating in caller's memory
+ * context.  Otherwise returns NULL, indicating that operator class has no
+ * skip support function.
+ */
+SkipSupport
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse)
+{
+	Oid			skipSupportFunction;
+	SkipSupport sksup;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return NULL;
+
+	sksup = palloc(sizeof(SkipSupportData));
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return sksup;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 9682f9dbd..347089b76 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2304,6 +2305,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 4f8402ef9..1b72059d2 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,6 +13,7 @@
 
 #include "postgres.h"
 
+#include <limits.h>
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
@@ -21,6 +22,7 @@
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -416,6 +418,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index 768b77aa0..d5adb58c1 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -835,7 +835,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index fd9bdd884..99ef54ddd 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4249,7 +4249,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage
      <replaceable>column_name</replaceable> =
      <replaceable>value2</replaceable> ...</literal> construct, though only
     when the optimizer transforms the construct into an equivalent
-    multi-valued array representation.
+    multi-valued array representation.  Similarly, when B-Tree index scans use
+    the skip scan strategy, an index search is performed each time the scan is
+    repositioned to the next index leaf page that might have matching tuples.
    </para>
   </note>
   <tip>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 053619624..7e23a7b6e 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index 0c274d56a..23bf33f10 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  ordering equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 8879554c3..bfb1a286e 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -581,6 +581,47 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Index Only Scan using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Index Only Scan Backward using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 --
 -- Test for multilevel page deletion
 --
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index bd5f002cf..15dde752f 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1637,7 +1637,9 @@ DROP TABLE syscol_table;
 -- Tests for IS NULL/IS NOT NULL with b-tree indexes
 --
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 SET enable_seqscan = OFF;
 SET enable_indexscan = ON;
@@ -1645,7 +1647,7 @@ SET enable_bitmapscan = ON;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1657,13 +1659,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1678,12 +1680,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1695,13 +1703,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1722,12 +1730,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0,
      1
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc nulls last,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1739,13 +1753,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1760,12 +1774,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2  nulls first,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1777,13 +1797,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1798,6 +1818,12 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 -- Check initial-positioning logic too
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2);
@@ -1829,20 +1855,24 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
 (2 rows)
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-         |        
-     278 |     999
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 5;
+ unique1 |  unique2   
+---------+------------
+     500 |           
+     100 |           
+         |           
+         | 2147483647
+     278 |        999
+(5 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-     278 |     999
-       0 |     998
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 3;
+ unique1 |  unique2   
+---------+------------
+         | 2147483647
+     278 |        999
+       0 |        998
+(3 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
@@ -2247,7 +2277,8 @@ SELECT count(*) FROM dupindexcols
 (1 row)
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 explain (costs off)
 SELECT unique1 FROM tenk1
@@ -2269,7 +2300,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2289,7 +2320,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2309,6 +2340,25 @@ ORDER BY thousand DESC, tenthous DESC;
         0 |     3000
 (2 rows)
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ Index Only Scan Backward using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > 995) AND (tenthous = ANY ('{998,999}'::integer[])))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+ thousand | tenthous 
+----------+----------
+      999 |      999
+      998 |      998
+(2 rows)
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -2339,6 +2389,45 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 ---------
 (0 rows)
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+                   QUERY PLAN                    
+-------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY (NULL::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY ('{NULL,NULL,NULL}'::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: ((unique1 IS NULL) AND (unique1 IS NULL))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+ unique1 
+---------
+(0 rows)
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
                                 QUERY PLAN                                 
@@ -2462,6 +2551,44 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 ---------
 (0 rows)
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > '-1'::integer) AND (thousand >= 0) AND (tenthous = 3000))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+(1 row)
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand < 3) AND (thousand <= 2) AND (tenthous = 1001))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        1 |     1001
+(1 row)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 6543e90de..6ebd6265f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5331,9 +5331,10 @@ Function              | in_range(time without time zone,time without time zone,i
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql
index 670ad5c6e..68c61dbc7 100644
--- a/src/test/regress/sql/btree_index.sql
+++ b/src/test/regress/sql/btree_index.sql
@@ -327,6 +327,27 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 
 --
 -- Test for multilevel page deletion
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index be570da08..40ba3d65b 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -650,7 +650,9 @@ DROP TABLE syscol_table;
 --
 
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 
 SET enable_seqscan = OFF;
@@ -663,6 +665,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -675,6 +678,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NUL
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0, 1);
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -686,6 +690,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -697,6 +702,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -716,9 +722,9 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
   ORDER BY unique2 LIMIT 2;
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 5;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 3;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
 
@@ -852,7 +858,8 @@ SELECT count(*) FROM dupindexcols
   WHERE f1 BETWEEN 'WA' AND 'ZZZ' and id < 1000 and f1 ~<~ 'YX';
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 
 explain (costs off)
@@ -864,7 +871,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -874,7 +881,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -884,6 +891,15 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand DESC, tenthous DESC;
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -897,6 +913,21 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 
 SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('{33, 44}'::bigint[]);
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
 
@@ -942,6 +973,26 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
 SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d60ae7c72..58fcc7575 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -220,6 +220,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2708,6 +2709,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.47.2

v27-0005-Lower-nbtree-skip-array-maintenance-overhead.patchapplication/x-patch; name=v27-0005-Lower-nbtree-skip-array-maintenance-overhead.patchDownload
From 6d725de909d25bfb6f0b14e486f421f763ab4747 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v27 5/7] Lower nbtree skip array maintenance overhead.

Add an optimization that fixes regressions in index scans that are
nominally eligible to use skip scan, but can never actually benefit from
skipping.  These are cases where a leading prefix column contains many
distinct values -- especially when the number of values approaches the
total number of index tuples, where skipping can never be profitable.

The optimization is activated dynamically, as a fallback strategy.  It
works by determining a prefix of leading index columns whose scan keys
(often skip array scan keys) are guaranteed to be satisfied by every
possible index tuple on a given page.  _bt_readpage is then able to
start comparisons at the first scan key that might not be satisfied.
This necessitates making _bt_readpage temporarily cease maintaining the
scan's arrays.  _bt_checkkeys will treat the scan's keys as if they were
not marked as required during preprocessing.  This process relies on the
non-required SAOP array logic in _bt_advance_array_keys that was added
to Postgres 17 by commit 5bf748b8.

The new optimization does not affect array primitive scan scheduling.
It is similar to the precheck optimization added by Postgres 17 commit
e0b1ee17dc, though it is only used during nbtree scans with skip arrays.
It can be applied during scans that were never eligible for the precheck
optimization.  As a result, many scans that cannot benefit from skipping
will still benefit from using skip arrays (skip arrays indirectly enable
the use of the optimization introduced by this commit).

Skip scan's approach of adding skip arrays during preprocessing and then
fixing (or significantly ameliorating) the resulting regressions seen in
unsympathetic cases is enabled by the optimization added by this commit
(and by the "look ahead" optimization introduced by commit 5bf748b8).
This allows the planner to avoid generating distinct, competing index
paths (one path for skip scan, another for an equivalent traditional
full index scan).  The overall effect is to make scan runtime close to
optimal, even when the planner works off an incorrect cardinality
estimate.  Scans will also perform well given a skipped column with data
skew: individual groups of pages with many distinct values in respect of
a skipped column can be read about as efficiently as before, without
having to give up on skipping over other provably-irrelevant leaf pages.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h           |   5 +-
 src/backend/access/nbtree/nbtsearch.c |  48 ++++
 src/backend/access/nbtree/nbtutils.c  | 395 +++++++++++++++++++++++---
 3 files changed, 401 insertions(+), 47 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 7b766da20..f25f4b05d 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1115,11 +1115,13 @@ typedef struct BTReadPageState
 
 	/*
 	 * Input and output parameters, set and unset by both _bt_readpage and
-	 * _bt_checkkeys to manage precheck optimizations
+	 * _bt_checkkeys to manage precheck and forcenonrequired optimizations
 	 */
 	bool		firstpage;		/* on first page of current primitive scan? */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
+	bool		forcenonrequired;	/* treat all scan keys as nonrequired? */
+	int			ikey;			/* start comparisons from ikey'th scan key */
 
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
@@ -1328,6 +1330,7 @@ extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arra
 						  IndexTuple tuple, int tupnatts);
 extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
 									 IndexTuple finaltup);
+extern void _bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 27abb7ef9..29ccd9198 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1651,6 +1651,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.firstpage = firstpage;
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
+	pstate.forcenonrequired = false;
+	pstate.ikey = 0;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
@@ -1733,6 +1735,14 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 													   so->currPos.currPage);
 					return false;
 				}
+
+				/*
+				 * Use pstate.ikey optimization during primitive index scans
+				 * with skip arrays when reading a second or subsequent page
+				 * (unless we've reached the rightmost page)
+				 */
+				if (!pstate.firstpage && so->skipScan && minoff < maxoff)
+					_bt_skip_ikeyprefix(scan, &pstate);
 			}
 
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
@@ -1774,6 +1784,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum < pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1837,6 +1848,15 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			IndexTuple	itup = (IndexTuple) PageGetItem(page, iid);
 			int			truncatt;
 
+			if (pstate.forcenonrequired)
+			{
+				Assert(so->skipScan);
+
+				/* recover from treating the scan's keys as nonrequired */
+				_bt_start_array_keys(scan, dir);
+				pstate.forcenonrequired = false;
+				pstate.ikey = 0;
+			}
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
@@ -1873,6 +1893,14 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 													   so->currPos.currPage);
 					return false;
 				}
+
+				/*
+				 * Use pstate.ikey optimization during primitive index scans
+				 * with skip arrays when reading a second or subsequent page
+				 * (unless we've reached the leftmost page)
+				 */
+				if (!pstate.firstpage && so->skipScan && minoff < maxoff)
+					_bt_skip_ikeyprefix(scan, &pstate);
 			}
 
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
@@ -1917,6 +1945,15 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff && pstate.forcenonrequired)
+			{
+				Assert(so->skipScan);
+
+				/* recover from treating the scan's keys as nonrequired */
+				_bt_start_array_keys(scan, dir);
+				pstate.forcenonrequired = false;
+				pstate.ikey = 0;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
@@ -1928,6 +1965,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum > pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1993,6 +2031,16 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 		so->currPos.itemIndex = MaxTIDsPerBTreePage - 1;
 	}
 
+	/*
+	 * As far as our caller is concerned, the scan's arrays always track its
+	 * progress through the index's key space.
+	 *
+	 * If _bt_skip_ikeyprefix told us to temporarily treat all scan keys as
+	 * nonrequired (during a skip scan), then we must recover afterwards by
+	 * advancing our arrays using finaltup (with !pstate.forcenonrequired).
+	 */
+	Assert(pstate.ikey == 0 && !pstate.forcenonrequired);
+
 	return (so->currPos.firstItem <= so->currPos.lastItem);
 }
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 49afe779f..0ae1e05b9 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -57,11 +57,12 @@ static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool forcenonrequired,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-								 ScanDirection dir, bool *continuescan);
+								 ScanDirection dir, bool forcenonrequired, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -1421,14 +1422,14 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
  * postcondition's <= operator with a >=.  In other words, just swap the
  * precondition with the postcondition.)
  *
- * We also deal with "advancing" non-required arrays here.  Callers whose
- * sktrig scan key is non-required specify sktrig_required=false.  These calls
- * are the only exception to the general rule about always advancing the
+ * We also deal with "advancing" non-required arrays here (or arrays that are
+ * treated as non-required for the duration of a _bt_readpage call).  Callers
+ * whose sktrig scan key is non-required specify sktrig_required=false.  These
+ * calls are the only exception to the general rule about always advancing the
  * required array keys (the scan may not even have a required array).  These
  * callers should just pass a NULL pstate (since there is never any question
  * of stopping the scan).  No call to _bt_tuple_before_array_skeys is required
- * ahead of these calls (it's already clear that any required scan keys must
- * be satisfied by caller's tuple).
+ * ahead of these calls.
  *
  * Note that we deal with non-array required equality strategy scan keys as
  * degenerate single element arrays here.  Obviously, they can never really
@@ -1480,8 +1481,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		pstate->firstmatch = false;
 
-		/* Shouldn't have to invalidate 'prechecked', though */
-		Assert(!pstate->prechecked);
+		/* Shouldn't have to invalidate precheck/forcenonrequired state */
+		Assert(!pstate->prechecked && !pstate->forcenonrequired &&
+			   pstate->ikey == 0);
 
 		/*
 		 * Once we return we'll have a new set of required array keys, so
@@ -1490,6 +1492,26 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		pstate->rechecks = 0;
 		pstate->targetdistance = 0;
 	}
+	else if (sktrig < so->numberOfKeys - 1 &&
+			 !(so->keyData[so->numberOfKeys - 1].sk_flags & SK_SEARCHARRAY))
+	{
+		int			least_sign_ikey = so->numberOfKeys - 1;
+		bool		continuescan;
+
+		/*
+		 * Optimization: perform a precheck of the least significant key
+		 * during !sktrig_required calls when it isn't already our sktrig
+		 * (provided the precheck key is not itself an array).
+		 *
+		 * When the precheck works out we'll avoid an expensive binary search
+		 * of sktrig's array (plus any other arrays before least_sign_ikey).
+		 */
+		Assert(so->keyData[sktrig].sk_flags & SK_SEARCHARRAY);
+		if (!_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
+							   false, false, false, false,
+							   &continuescan, &least_sign_ikey))
+			return false;
+	}
 
 	Assert(_bt_verify_keys_with_arraykeys(scan));
 
@@ -1533,8 +1555,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -1668,7 +1688,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -1710,7 +1730,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -1725,7 +1745,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -1785,6 +1805,12 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * of any required scan key).  All that matters is whether caller's tuple
 	 * satisfies the new qual, so it's safe to just skip the _bt_check_compare
 	 * recheck when we've already determined that it can only return 'false'.
+	 *
+	 * Note: In practice most scan keys are marked required by preprocessing,
+	 * if necessary by generating a preceding skip array.  We nevertheless
+	 * often handle array keys marked required as if they were nonrequired.
+	 * This behavior is requested by our _bt_check_compare caller, though only
+	 * when it is passed "forcenonrequired=true" by _bt_checkkeys.
 	 */
 	if ((sktrig_required && all_required_satisfied) ||
 		(!sktrig_required && all_satisfied))
@@ -1796,7 +1822,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -2040,20 +2066,23 @@ new_prim_scan:
 	 * the scan arrives on the next sibling leaf page) when it has already
 	 * read at least one leaf page before the one we're reading now.  This is
 	 * important when reading subsets of an index with many distinct values in
-	 * respect of an attribute constrained by an array.  It encourages fewer,
-	 * larger primitive scans where that makes sense.
-	 *
-	 * Note: so->scanBehind is primarily used to indicate that the scan
-	 * encountered a finaltup that "satisfied" one or more required scan keys
-	 * on a truncated attribute value/-inf value.  We can safely reuse it to
-	 * force the scan to stay on the leaf level because the considerations are
-	 * exactly the same.
+	 * respect of an attribute constrained by an array (often a skip array).
+	 * It encourages fewer, larger primitive scans where that makes sense.
+	 * This will in turn encourage _bt_readpage to apply the forcenonrequired
+	 * optimization when applicable (i.e. when the scan has a skip array).
 	 *
 	 * Note: This heuristic isn't as aggressive as you might think.  We're
 	 * conservative about allowing a primitive scan to step from the first
 	 * leaf page it reads to the page's sibling page (we only allow it on
 	 * first pages whose finaltup strongly suggests that it'll work out).
 	 * Clearing this first page finaltup hurdle is a strong signal in itself.
+	 *
+	 * Note: so->scanBehind is primarily used to indicate that the scan
+	 * encountered a finaltup that "satisfied" one or more required scan keys
+	 * on a truncated attribute value/-inf value.  We reuse it to force the
+	 * scan to stay on the leaf level because the considerations are just the
+	 * same (the array's are ahead of the index key space, or they're behind
+	 * when we're scanning backwards).
 	 */
 	if (!pstate->firstpage)
 	{
@@ -2229,14 +2258,16 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	TupleDesc	tupdesc = RelationGetDescr(scan->indexRelation);
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ScanDirection dir = so->currPos.dir;
-	int			ikey = 0;
+	int			ikey = pstate->ikey;
 	bool		res;
 
+	Assert(ikey == 0 || pstate->forcenonrequired);
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+							arrayKeys, pstate->forcenonrequired,
+							pstate->prechecked, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
@@ -2247,12 +2278,12 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 *
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
-		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
+		Assert(!pstate->prechecked && !pstate->firstmatch &&
+			   !pstate->forcenonrequired);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
-	if (pstate->prechecked || pstate->firstmatch)
+	if ((pstate->prechecked || pstate->firstmatch) && !pstate->forcenonrequired)
 	{
 		bool		dcontinuescan;
 		int			dikey = 0;
@@ -2262,7 +2293,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, false, false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -2285,6 +2316,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * It's also possible that the scan is still _before_ the _start_ of
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
+	Assert(!pstate->forcenonrequired);
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
@@ -2400,7 +2432,7 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	Assert(so->numArrayKeys);
 
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -2408,6 +2440,231 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	return true;
 }
 
+/*
+ * Determine a prefix of scan keys that are guaranteed to be satisfied by
+ * every possible tuple on pstate's page.  Used during scans with skip arrays,
+ * which do not use the similar optimization controlled by pstate.prechecked.
+ *
+ * Sets pstate.ikey (and pstate.forcenonrequired) on success, making later
+ * calls to _bt_checkkeys start checks of each tuple from the so->keyData[]
+ * entry at pstate.ikey (while treating keys >= pstate.ikey as nonrequired).
+ *
+ * When _bt_checkkeys treats the scan's required keys as non-required, the
+ * scan's array keys won't be properly maintained (they won't have advanced in
+ * lockstep with our progress through the index's key space as expected).
+ * Caller must recover from this by restarting the scan's array keys and
+ * resetting pstate.ikey and pstate.forcenonrequired just ahead of the
+ * _bt_checkkeys call for the page's final tuple (the pstate.finaltup tuple).
+ */
+void
+_bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	ScanDirection dir = so->currPos.dir;
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	ItemId		iid;
+	IndexTuple	firsttup,
+				lasttup;
+	int			ikey = 0,
+				arrayidx = 0,
+				firstchangingattnum;
+
+	Assert(so->skipScan && pstate->minoff < pstate->maxoff);
+
+	/* minoff is an offset to the lowest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->minoff);
+	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* maxoff is an offset to the highest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->maxoff);
+	lasttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* Determine the first attribute whose values change on caller's page */
+	firstchangingattnum = _bt_keep_natts_fast(rel, firsttup, lasttup);
+
+	for (; ikey < so->numberOfKeys; ikey++)
+	{
+		ScanKey		key = so->keyData + ikey;
+		BTArrayKeyInfo *array;
+		Datum		tupdatum;
+		bool		tupnull;
+		int32		result;
+
+		/*
+		 * Determine if it's safe to set pstate.ikey to an offset to a key
+		 * that comes after this key, by examining this key
+		 */
+		if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) == 0)
+		{
+			/*
+			 * This is the first key that is not marked required (only happens
+			 * when _bt_preprocess_keys couldn't mark all keys required due to
+			 * implementation restrictions affecting skip array generation)
+			 */
+			Assert(!(key->sk_flags & SK_BT_SKIP));
+			break;				/* pstate.ikey to be set to nonrequired ikey */
+		}
+		if (key->sk_strategy != BTEqualStrategyNumber)
+		{
+			/*
+			 * This is the scan's first inequality key (barring inequalities
+			 * that are used to describe the range of a = skip array key).
+			 *
+			 * We could handle this like a = key, but it doesn't seem worth
+			 * the trouble.  Have _bt_checkkeys start with this inequality.
+			 */
+			break;				/* pstate.ikey to be set to inequality's ikey */
+		}
+		if (!(key->sk_flags & SK_SEARCHARRAY))
+		{
+			/*
+			 * Found a scalar (non-array) = key.
+			 *
+			 * It is unsafe to set pstate.ikey to an ikey beyond this key,
+			 * unless the = key is satisfied by every possible tuple on the
+			 * page (possible only when attribute has just one distinct value
+			 * among all tuples on the page).
+			 */
+			if (key->sk_attno < firstchangingattnum)
+			{
+				/*
+				 * Only one distinct value on the page for this key's column.
+				 * Must make sure that = key is actually satisfied by the
+				 * value that is stored within every tuple on the page.
+				 */
+				tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+										 &tupnull);
+				result = _bt_compare_array_skey(&so->orderProcs[ikey],
+												tupdatum, tupnull,
+												key->sk_argument, key);
+				if (result == 0)
+					continue;	/* safe, = key satisfied by every tuple */
+			}
+			break;				/* pstate.ikey to be set to scalar key's ikey */
+		}
+
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == ikey);
+		if (array->num_elems != -1)
+		{
+			/*
+			 * Found a SAOP array = key.
+			 *
+			 * Handle this just like we handle scalar = keys.
+			 */
+			if (key->sk_attno < firstchangingattnum)
+			{
+				/*
+				 * Only one distinct value on the page for this key's column.
+				 * Must make sure that SAOP array is actually satisfied by the
+				 * value that is stored within every tuple on the page.
+				 */
+				tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+										 &tupnull);
+				_bt_binsrch_array_skey(&so->orderProcs[ikey], false,
+									   NoMovementScanDirection,
+									   tupdatum, tupnull, array, key, &result);
+				if (result == 0)
+					continue;	/* safe, SAOP = key satisfied by every tuple */
+			}
+			break;				/* pstate.ikey to be set to SAOP array's ikey */
+		}
+
+		/*
+		 * Found a skip array = key.
+		 *
+		 * As with other = keys, moving past this skip array key is safe when
+		 * every tuple on the page is guaranteed to satisfy the array's key.
+		 * But we need a slightly different approach, since skip arrays make
+		 * it easy to assess whether all the values on the page fall within
+		 * the skip array's entire range.
+		 */
+		if (array->null_elem)
+		{
+			/* Safe, non-range skip array "satisfied" by every tuple on page */
+			continue;
+		}
+		else if (key->sk_attno > firstchangingattnum)
+		{
+			/*
+			 * We cannot assess whether this range skip array will definitely
+			 * be satisfied by every tuple on the page, since its attribute is
+			 * preceded by another attribute that is not certain to contain
+			 * the same prefix of value(s) within every tuple from pstate.page
+			 */
+			break;				/* pstate.ikey to be set to range array's ikey */
+		}
+
+		/*
+		 * Found a range skip array = key.
+		 *
+		 * It's definitely safe for _bt_checkkeys to avoid assessing this
+		 * range skip array when the page's first and last non-pivot tuples
+		 * both satisfy the range skip array (since the same must also be true
+		 * of all the tuples in between these two).
+		 */
+		tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   tupdatum, tupnull, array, key, &result);
+		if (result != 0)
+			break;				/* pstate.ikey to be set to range array's ikey */
+
+		tupdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   tupdatum, tupnull, array, key, &result);
+		if (result != 0)
+			break;				/* pstate.ikey to be set to range array's ikey */
+
+		/* Safe, range skip array satisfied by every tuple on page */
+	}
+
+	/*
+	 * If pstate.ikey remains 0, _bt_advance_array_keys will still be able to
+	 * apply its precheck optimization when dealing with "nonrequired" array
+	 * keys.  That's reason enough on its own to set forcenonrequired=true.
+	 */
+	pstate->forcenonrequired = true;	/* do this unconditionally */
+	pstate->ikey = ikey;
+
+	/*
+	 * Set the element for range skip arrays whose ikey is >= pstate.ikey to
+	 * whatever the first array element is in the scan's current direction.
+	 * This allows range skip arrays that will never be satisfied by any tuple
+	 * on the page to avoid extra sk_argument comparisons -- _bt_check_compare
+	 * won't use the key's sk_argument when the key is marked MINVAL/MAXVAL
+	 * (note that MINVAL/MAXVAL won't be unset until an exact match is found,
+	 * which might not happen for any tuple on the page).
+	 *
+	 * Set the element for non-range skip arrays whose ikey is >= pstate.ikey
+	 * to NULL (regardless of whether NULLs are stored first or last), too.
+	 * This allows non-range skip arrays (recognized by _bt_check_compare as
+	 * "non-required" skip arrays with ISNULL set) to avoid needlessly calling
+	 * _bt_advance_array_keys (we know that any non-range skip array must be
+	 * satisfied by every possible indexable value, so this is always safe).
+	 */
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		key = &so->keyData[array->scan_key];
+
+		Assert(key->sk_flags & SK_SEARCHARRAY);
+
+		if (array->num_elems != -1)
+			continue;
+		if (array->scan_key < pstate->ikey)
+			continue;
+
+		Assert(key->sk_flags & SK_BT_SKIP);
+
+		if (!array->null_elem)
+			_bt_array_set_low_or_high(rel, key, array,
+									  ScanDirectionIsForward(dir));
+		else
+			_bt_skiparray_set_isnull(rel, key, array);
+	}
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
@@ -2439,17 +2696,25 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Though we advance non-required array keys on our own, that shouldn't have
  * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
+ * have no fixed relationship with the scan's progress.
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass forcenonrequired=true to instruct us to treat all keys as nonrequried.
+ * This is used to make it safe to temporarily stop properly maintaining the
+ * scan's required arrays.  Callers can determine which prefix of keys must
+ * satisfy every possible prefix of index attribute values on the page, and
+ * then pass us an initial *ikey for the first key that might be unsatisified.
+ * We won't be maintaining any arrays before that initial *ikey, so there is
+ * no point in trying to do so for any later arrays.  (Callers that do this
+ * must be careful to reset the array keys when they finish reading the page.)
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool forcenonrequired,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -2466,10 +2731,13 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when we're forced to treat all scan keys as nonrequired)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (forcenonrequired)
+			Assert(!prechecked);
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
@@ -2517,6 +2785,19 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		{
 			Assert(key->sk_flags & SK_SEARCHARRAY);
 			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir || forcenonrequired);
+
+			/*
+			 * Cannot fall back on _bt_tuple_before_array_skeys when we're
+			 * treating the scan's keys as nonrequired, though.  Just handle
+			 * this like any other non-required equality-type array key.
+			 */
+			if (forcenonrequired)
+			{
+				Assert(!(key->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+				return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
+											  tupdesc, *ikey, false);
+			}
 
 			*continuescan = false;
 			return false;
@@ -2526,7 +2807,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
-									 continuescan))
+									 forcenonrequired, continuescan))
 				continue;
 			return false;
 		}
@@ -2558,9 +2839,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			 */
 			if (requiredSameDir)
 				*continuescan = false;
+			else if (unlikely(key->sk_flags & SK_BT_SKIP))
+			{
+				/*
+				 * If we're treating scan keys as nonrequired, and encounter a
+				 * skip array scan key whose current element is NULL, then it
+				 * must be a non-range skip array.  It must be satisfied, so
+				 * there's no need to call _bt_advance_array_keys to check.
+				 */
+				Assert(forcenonrequired && *ikey > 0);
+				continue;
+			}
 
 			/*
-			 * In any case, this indextuple doesn't match the qual.
+			 * This indextuple doesn't match the qual.
 			 */
 			return false;
 		}
@@ -2581,7 +2873,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -2599,7 +2891,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -2668,7 +2960,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
  */
 static bool
 _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
-					 TupleDesc tupdesc, ScanDirection dir, bool *continuescan)
+					 TupleDesc tupdesc, ScanDirection dir,
+					 bool forcenonrequired, bool *continuescan)
 {
 	ScanKey		subkey = (ScanKey) DatumGetPointer(skey->sk_argument);
 	int32		cmpresult = 0;
@@ -2708,7 +3001,11 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 
 		if (isNull)
 		{
-			if (subkey->sk_flags & SK_BT_NULLS_FIRST)
+			if (forcenonrequired)
+			{
+				/* treating scan key as non-required */
+			}
+			else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -2762,8 +3059,12 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			 */
 			Assert(subkey != (ScanKey) DatumGetPointer(skey->sk_argument));
 			subkey--;
-			if ((subkey->sk_flags & SK_BT_REQFWD) &&
-				ScanDirectionIsForward(dir))
+			if (forcenonrequired)
+			{
+				/* treating scan key as non-required */
+			}
+			else if ((subkey->sk_flags & SK_BT_REQFWD) &&
+					 ScanDirectionIsForward(dir))
 				*continuescan = false;
 			else if ((subkey->sk_flags & SK_BT_REQBKWD) &&
 					 ScanDirectionIsBackward(dir))
@@ -2815,7 +3116,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			break;
 	}
 
-	if (!result)
+	if (!result && !forcenonrequired)
 	{
 		/*
 		 * Tuple fails this qual.  If it's a required qual for the current
@@ -2859,6 +3160,8 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	Assert(!pstate->forcenonrequired);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
-- 
2.47.2

v27-0001-Show-index-search-count-in-EXPLAIN-ANALYZE-take-.patchapplication/x-patch; name=v27-0001-Show-index-search-count-in-EXPLAIN-ANALYZE-take-.patchDownload
From c139ebfdd3703585ae46cdc5348a23ac528908bc Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 5 Mar 2025 09:36:48 -0500
Subject: [PATCH v27 1/7] Show index search count in EXPLAIN ANALYZE, take 2.

Expose the count of index searches/index descents in EXPLAIN ANALYZE's
output for index scan/index-only scan/bitmap index scan nodes.  This
information is particularly useful with scans that use ScalarArrayOp
quals, where the number of index scans isn't predictable (at least not
with optimizations like the one added by Postgres 17 commit 5bf748b8).
It will also be useful when EXPLAIN ANALYZE shows details of an nbtree
index scan that uses skip scan optimizations set to be introduced by an
upcoming patch.

The instrumentation works by teaching all index AMs to increment a new
nsearches counter whenever a new index search begins.  The counter is
incremented at exactly the same point that index AMs already increment
the pg_stat_*_indexes.idx_scan counter (we're counting the same event,
but at the scan level rather than the relation level).  Parallel index
scans have parallel workers copy the counter into shared memory, even
when parallel workers run an index scan node that isn't parallel aware.
(This addresses an oversight in an earlier committed version that was to
immediate reverted in commit d00107cd).

Our approach doesn't match the approach used when tracking other index
scan specific costs (e.g., "Rows Removed by Filter:").  It is similar to
the approach used in other cases where we must track costs that are only
readily accessible inside an access method, and not from the executor
(e.g., "Heap Blocks:" output for a Bitmap Heap Scan).  It is inherently
necessary to maintain a counter that can be incremented multiple times
during a single amgettuple call (or amgetbitmap call), which makes
passing down PlanState.instrument to amgettuple routines unappealing.
Index access methods work off of a dedicated instrumentation struct,
which could easily be expanded to track other kinds of execution costs.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Robert Haas <robertmhaas@gmail.com>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Discussion: https://postgr.es/m/CAH2-Wz=PKR6rB7qbx+Vnd7eqeB5VTcrW=iJvAsTsKbdG+kW_UA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
---
 src/include/access/genam.h                    |  33 ++++-
 src/include/access/relscan.h                  |  11 +-
 src/include/executor/nodeBitmapIndexscan.h    |   6 +
 src/include/executor/nodeIndexonlyscan.h      |   1 +
 src/include/executor/nodeIndexscan.h          |   1 +
 src/include/nodes/execnodes.h                 |  16 ++
 src/backend/access/brin/brin.c                |   2 +
 src/backend/access/gin/ginscan.c              |   2 +
 src/backend/access/gist/gistget.c             |   4 +
 src/backend/access/hash/hashsearch.c          |   2 +
 src/backend/access/heap/heapam_handler.c      |   2 +-
 src/backend/access/index/genam.c              |   5 +-
 src/backend/access/index/indexam.c            |  62 +++++---
 src/backend/access/nbtree/nbtree.c            |  10 +-
 src/backend/access/nbtree/nbtsearch.c         |   2 +
 src/backend/access/spgist/spgscan.c           |   2 +
 src/backend/commands/explain.c                |  63 ++++++++
 src/backend/executor/execIndexing.c           |   2 +-
 src/backend/executor/execParallel.c           |  59 +++++---
 src/backend/executor/execReplication.c        |   2 +-
 src/backend/executor/nodeBitmapIndexscan.c    | 111 ++++++++++++++
 src/backend/executor/nodeIndexonlyscan.c      | 138 +++++++++++++-----
 src/backend/executor/nodeIndexscan.c          | 136 +++++++++++++----
 src/backend/utils/adt/selfuncs.c              |   2 +-
 contrib/bloom/blscan.c                        |   2 +
 doc/src/sgml/bloom.sgml                       |   7 +-
 doc/src/sgml/monitoring.sgml                  |  28 +++-
 doc/src/sgml/perform.sgml                     |  60 ++++++++
 doc/src/sgml/ref/explain.sgml                 |   3 +-
 doc/src/sgml/rules.sgml                       |   2 +
 src/test/regress/expected/brin_multi.out      |  27 ++--
 src/test/regress/expected/memoize.out         |  49 +++++--
 src/test/regress/expected/partition_prune.out | 100 +++++++++++--
 src/test/regress/expected/select.out          |   3 +-
 src/test/regress/sql/memoize.sql              |   5 +-
 src/test/regress/sql/partition_prune.sql      |   4 +
 src/tools/pgindent/typedefs.list              |   2 +
 37 files changed, 803 insertions(+), 163 deletions(-)

diff --git a/src/include/access/genam.h b/src/include/access/genam.h
index 1be873957..437b31457 100644
--- a/src/include/access/genam.h
+++ b/src/include/access/genam.h
@@ -85,6 +85,27 @@ typedef struct IndexBulkDeleteResult
 	BlockNumber pages_free;		/* # pages available for reuse */
 } IndexBulkDeleteResult;
 
+/*
+ * Data structure for reporting index scan statistics that are maintained by
+ * index scans.  Note that IndexScanInstrumentation can't contain any pointers
+ * because it might need to be copied into a SharedIndexScanInstrumentation.
+ */
+typedef struct IndexScanInstrumentation
+{
+	/* Index search count (increment after calling pgstat_count_index_scan) */
+	uint64		nsearches;
+} IndexScanInstrumentation;
+
+/* ----------------
+ * Shared memory container for per-worker index scan information
+ * ----------------
+ */
+typedef struct SharedIndexScanInstrumentation
+{
+	int			num_workers;
+	IndexScanInstrumentation winstrument[FLEXIBLE_ARRAY_MEMBER];
+}			SharedIndexScanInstrumentation;
+
 /* Typedef for callback function to determine if a tuple is bulk-deletable */
 typedef bool (*IndexBulkDeleteCallback) (ItemPointer itemptr, void *state);
 
@@ -157,9 +178,11 @@ extern void index_insert_cleanup(Relation indexRelation,
 extern IndexScanDesc index_beginscan(Relation heapRelation,
 									 Relation indexRelation,
 									 Snapshot snapshot,
+									 IndexScanInstrumentation *instrument,
 									 int nkeys, int norderbys);
 extern IndexScanDesc index_beginscan_bitmap(Relation indexRelation,
 											Snapshot snapshot,
+											IndexScanInstrumentation *instrument,
 											int nkeys);
 extern void index_rescan(IndexScanDesc scan,
 						 ScanKey keys, int nkeys,
@@ -168,14 +191,18 @@ extern void index_endscan(IndexScanDesc scan);
 extern void index_markpos(IndexScanDesc scan);
 extern void index_restrpos(IndexScanDesc scan);
 extern Size index_parallelscan_estimate(Relation indexRelation,
-										int nkeys, int norderbys, Snapshot snapshot);
+										int nkeys, int norderbys, Snapshot snapshot,
+										bool instrument, int nworkers,
+										Size *instroffset);
 extern void index_parallelscan_initialize(Relation heapRelation,
 										  Relation indexRelation, Snapshot snapshot,
-										  ParallelIndexScanDesc target);
+										  ParallelIndexScanDesc target,
+										  Size ps_offset_ins);
 extern void index_parallelrescan(IndexScanDesc scan);
 extern IndexScanDesc index_beginscan_parallel(Relation heaprel,
 											  Relation indexrel, int nkeys, int norderbys,
-											  ParallelIndexScanDesc pscan);
+											  ParallelIndexScanDesc pscan,
+											  IndexScanInstrumentation *instrument);
 extern ItemPointer index_getnext_tid(IndexScanDesc scan,
 									 ScanDirection direction);
 struct TupleTableSlot;
diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index dc6e01842..1b65d44db 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -123,6 +123,8 @@ typedef struct IndexFetchTableData
 	Relation	rel;
 } IndexFetchTableData;
 
+struct IndexScanInstrumentation;
+
 /*
  * We use the same IndexScanDescData structure for both amgettuple-based
  * and amgetbitmap-based index scans.  Some fields are only relevant in
@@ -150,6 +152,12 @@ typedef struct IndexScanDescData
 	/* index access method's private state */
 	void	   *opaque;			/* access-method-specific info */
 
+	/*
+	 * Instrumentation counters that are maintained by every index access
+	 * method, for all scan types (except when instrument is set to NULL)
+	 */
+	struct IndexScanInstrumentation *instrument;
+
 	/*
 	 * In an index-only scan, a successful amgettuple call must fill either
 	 * xs_itup (and xs_itupdesc) or xs_hitup (and xs_hitupdesc) to provide the
@@ -188,7 +196,8 @@ typedef struct ParallelIndexScanDescData
 {
 	RelFileLocator ps_locator;	/* physical table relation to scan */
 	RelFileLocator ps_indexlocator; /* physical index relation to scan */
-	Size		ps_offset;		/* Offset in bytes of am specific structure */
+	Size		ps_offset_am;	/* Offset in bytes to am-specific structure */
+	Size		ps_offset_ins;	/* Offset to SharedIndexScanInstrumentation */
 	char		ps_snapshot_data[FLEXIBLE_ARRAY_MEMBER];
 }			ParallelIndexScanDescData;
 
diff --git a/src/include/executor/nodeBitmapIndexscan.h b/src/include/executor/nodeBitmapIndexscan.h
index b51cb184e..b6a5ae25e 100644
--- a/src/include/executor/nodeBitmapIndexscan.h
+++ b/src/include/executor/nodeBitmapIndexscan.h
@@ -14,11 +14,17 @@
 #ifndef NODEBITMAPINDEXSCAN_H
 #define NODEBITMAPINDEXSCAN_H
 
+#include "access/parallel.h"
 #include "nodes/execnodes.h"
 
 extern BitmapIndexScanState *ExecInitBitmapIndexScan(BitmapIndexScan *node, EState *estate, int eflags);
 extern Node *MultiExecBitmapIndexScan(BitmapIndexScanState *node);
 extern void ExecEndBitmapIndexScan(BitmapIndexScanState *node);
 extern void ExecReScanBitmapIndexScan(BitmapIndexScanState *node);
+extern void ExecBitmapIndexScanEstimate(BitmapIndexScanState *node, ParallelContext *pcxt);
+extern void ExecBitmapIndexScanInitializeDSM(BitmapIndexScanState *node, ParallelContext *pcxt);
+extern void ExecBitmapIndexScanInitializeWorker(BitmapIndexScanState *node,
+												ParallelWorkerContext *pwcxt);
+extern void ExecBitmapIndexScanRetrieveInstrumentation(BitmapIndexScanState *node);
 
 #endif							/* NODEBITMAPINDEXSCAN_H */
diff --git a/src/include/executor/nodeIndexonlyscan.h b/src/include/executor/nodeIndexonlyscan.h
index c27d8eb6d..ae85dee6d 100644
--- a/src/include/executor/nodeIndexonlyscan.h
+++ b/src/include/executor/nodeIndexonlyscan.h
@@ -32,5 +32,6 @@ extern void ExecIndexOnlyScanReInitializeDSM(IndexOnlyScanState *node,
 											 ParallelContext *pcxt);
 extern void ExecIndexOnlyScanInitializeWorker(IndexOnlyScanState *node,
 											  ParallelWorkerContext *pwcxt);
+extern void ExecIndexOnlyScanRetrieveInstrumentation(IndexOnlyScanState *node);
 
 #endif							/* NODEINDEXONLYSCAN_H */
diff --git a/src/include/executor/nodeIndexscan.h b/src/include/executor/nodeIndexscan.h
index 1c63d0615..08f0a148d 100644
--- a/src/include/executor/nodeIndexscan.h
+++ b/src/include/executor/nodeIndexscan.h
@@ -28,6 +28,7 @@ extern void ExecIndexScanInitializeDSM(IndexScanState *node, ParallelContext *pc
 extern void ExecIndexScanReInitializeDSM(IndexScanState *node, ParallelContext *pcxt);
 extern void ExecIndexScanInitializeWorker(IndexScanState *node,
 										  ParallelWorkerContext *pwcxt);
+extern void ExecIndexScanRetrieveInstrumentation(IndexScanState *node);
 
 /*
  * These routines are exported to share code with nodeIndexonlyscan.c and
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index a323fa98b..c0a983718 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1680,6 +1680,8 @@ typedef struct
  *		RuntimeContext	   expr context for evaling runtime Skeys
  *		RelationDesc	   index relation descriptor
  *		ScanDesc		   index scan descriptor
+ *		Instrument		   local index scan instrumentation
+ *		SharedInfo		   statistics for parallel workers
  *
  *		ReorderQueue	   tuples that need reordering due to re-check
  *		ReachedEnd		   have we fetched all tuples from index already?
@@ -1689,6 +1691,7 @@ typedef struct
  *		OrderByTypByVals   is the datatype of order by expression pass-by-value?
  *		OrderByTypLens	   typlens of the datatypes of order by expressions
  *		PscanLen		   size of parallel index scan descriptor
+ *		PscanInstrOffset   offset to SharedInfo (only used in leader)
  * ----------------
  */
 typedef struct IndexScanState
@@ -1706,6 +1709,8 @@ typedef struct IndexScanState
 	ExprContext *iss_RuntimeContext;
 	Relation	iss_RelationDesc;
 	struct IndexScanDescData *iss_ScanDesc;
+	IndexScanInstrumentation iss_Instrument;
+	SharedIndexScanInstrumentation *iss_SharedInfo;
 
 	/* These are needed for re-checking ORDER BY expr ordering */
 	pairingheap *iss_ReorderQueue;
@@ -1716,6 +1721,7 @@ typedef struct IndexScanState
 	bool	   *iss_OrderByTypByVals;
 	int16	   *iss_OrderByTypLens;
 	Size		iss_PscanLen;
+	Size		iss_PscanInstrOffset;
 } IndexScanState;
 
 /* ----------------
@@ -1732,9 +1738,12 @@ typedef struct IndexScanState
  *		RuntimeContext	   expr context for evaling runtime Skeys
  *		RelationDesc	   index relation descriptor
  *		ScanDesc		   index scan descriptor
+ *		Instrument		   local index scan instrumentation
+ *		SharedInfo		   statistics for parallel workers
  *		TableSlot		   slot for holding tuples fetched from the table
  *		VMBuffer		   buffer in use for visibility map testing, if any
  *		PscanLen		   size of parallel index-only scan descriptor
+ *		PscanInstrOffset   offset to SharedInfo (only used in leader)
  *		NameCStringAttNums attnums of name typed columns to pad to NAMEDATALEN
  *		NameCStringCount   number of elements in the NameCStringAttNums array
  * ----------------
@@ -1753,9 +1762,12 @@ typedef struct IndexOnlyScanState
 	ExprContext *ioss_RuntimeContext;
 	Relation	ioss_RelationDesc;
 	struct IndexScanDescData *ioss_ScanDesc;
+	IndexScanInstrumentation ioss_Instrument;
+	SharedIndexScanInstrumentation *ioss_SharedInfo;
 	TupleTableSlot *ioss_TableSlot;
 	Buffer		ioss_VMBuffer;
 	Size		ioss_PscanLen;
+	Size		ioss_PscanInstrOffset;
 	AttrNumber *ioss_NameCStringAttNums;
 	int			ioss_NameCStringCount;
 } IndexOnlyScanState;
@@ -1774,6 +1786,8 @@ typedef struct IndexOnlyScanState
  *		RuntimeContext	   expr context for evaling runtime Skeys
  *		RelationDesc	   index relation descriptor
  *		ScanDesc		   index scan descriptor
+ *		Instrument		   local index scan instrumentation
+ *		SharedInfo		   statistics for parallel workers
  * ----------------
  */
 typedef struct BitmapIndexScanState
@@ -1790,6 +1804,8 @@ typedef struct BitmapIndexScanState
 	ExprContext *biss_RuntimeContext;
 	Relation	biss_RelationDesc;
 	struct IndexScanDescData *biss_ScanDesc;
+	IndexScanInstrumentation biss_Instrument;
+	SharedIndexScanInstrumentation *biss_SharedInfo;
 } BitmapIndexScanState;
 
 /* ----------------
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index b01009c5d..737ad6388 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -592,6 +592,8 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
 	pgstat_count_index_scan(idxRel);
+	if (scan->instrument)
+		scan->instrument->nsearches++;
 
 	/*
 	 * We need to know the size of the table so that we know how long to
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index 84aa14594..f6cdd098a 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -442,6 +442,8 @@ ginNewScanKey(IndexScanDesc scan)
 	MemoryContextSwitchTo(oldCtx);
 
 	pgstat_count_index_scan(scan->indexRelation);
+	if (scan->instrument)
+		scan->instrument->nsearches++;
 }
 
 void
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index cc40e928e..387d99723 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -625,6 +625,8 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir)
 		GISTSearchItem fakeItem;
 
 		pgstat_count_index_scan(scan->indexRelation);
+		if (scan->instrument)
+			scan->instrument->nsearches++;
 
 		so->firstCall = false;
 		so->curPageData = so->nPageData = 0;
@@ -750,6 +752,8 @@ gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		return 0;
 
 	pgstat_count_index_scan(scan->indexRelation);
+	if (scan->instrument)
+		scan->instrument->nsearches++;
 
 	/* Begin the scan by processing the root page */
 	so->curPageData = so->nPageData = 0;
diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c
index a3a1fccf3..92c15a65b 100644
--- a/src/backend/access/hash/hashsearch.c
+++ b/src/backend/access/hash/hashsearch.c
@@ -298,6 +298,8 @@ _hash_first(IndexScanDesc scan, ScanDirection dir)
 	HashScanPosItem *currItem;
 
 	pgstat_count_index_scan(rel);
+	if (scan->instrument)
+		scan->instrument->nsearches++;
 
 	/*
 	 * We do not support hash scans with no index qualification, because we
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index e78682c3c..d74f0fbc5 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -749,7 +749,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 
 		tableScan = NULL;
 		heapScan = NULL;
-		indexScan = index_beginscan(OldHeap, OldIndex, SnapshotAny, 0, 0);
+		indexScan = index_beginscan(OldHeap, OldIndex, SnapshotAny, NULL, 0, 0);
 		index_rescan(indexScan, NULL, 0, NULL, 0);
 	}
 	else
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 07bae342e..886c05655 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -119,6 +119,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->ignore_killed_tuples = !scan->xactStartedInRecovery;
 
 	scan->opaque = NULL;
+	scan->instrument = NULL;
 
 	scan->xs_itup = NULL;
 	scan->xs_itupdesc = NULL;
@@ -446,7 +447,7 @@ systable_beginscan(Relation heapRelation,
 		}
 
 		sysscan->iscan = index_beginscan(heapRelation, irel,
-										 snapshot, nkeys, 0);
+										 snapshot, NULL, nkeys, 0);
 		index_rescan(sysscan->iscan, idxkey, nkeys, NULL, 0);
 		sysscan->scan = NULL;
 
@@ -711,7 +712,7 @@ systable_beginscan_ordered(Relation heapRelation,
 	}
 
 	sysscan->iscan = index_beginscan(heapRelation, indexRelation,
-									 snapshot, nkeys, 0);
+									 snapshot, NULL, nkeys, 0);
 	index_rescan(sysscan->iscan, idxkey, nkeys, NULL, 0);
 	sysscan->scan = NULL;
 
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 8b1f55543..073d58bd2 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -256,6 +256,7 @@ IndexScanDesc
 index_beginscan(Relation heapRelation,
 				Relation indexRelation,
 				Snapshot snapshot,
+				IndexScanInstrumentation *instrument,
 				int nkeys, int norderbys)
 {
 	IndexScanDesc scan;
@@ -270,6 +271,7 @@ index_beginscan(Relation heapRelation,
 	 */
 	scan->heapRelation = heapRelation;
 	scan->xs_snapshot = snapshot;
+	scan->instrument = instrument;
 
 	/* prepare to fetch index matches from table */
 	scan->xs_heapfetch = table_index_fetch_begin(heapRelation);
@@ -286,6 +288,7 @@ index_beginscan(Relation heapRelation,
 IndexScanDesc
 index_beginscan_bitmap(Relation indexRelation,
 					   Snapshot snapshot,
+					   IndexScanInstrumentation *instrument,
 					   int nkeys)
 {
 	IndexScanDesc scan;
@@ -299,6 +302,7 @@ index_beginscan_bitmap(Relation indexRelation,
 	 * up by RelationGetIndexScan.
 	 */
 	scan->xs_snapshot = snapshot;
+	scan->instrument = instrument;
 
 	return scan;
 }
@@ -448,20 +452,26 @@ index_restrpos(IndexScanDesc scan)
 
 /*
  * index_parallelscan_estimate - estimate shared memory for parallel scan
+ *
+ * Sets *instroffset to the offset into shared memory that caller should store
+ * the scan's SharedIndexScanInstrumentation state.  This is set to 0 when no
+ * instrumentation is required/allocated.
  */
 Size
 index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
-							Snapshot snapshot)
+							Snapshot snapshot, bool instrument, int nworkers,
+							Size *instroffset)
 {
-	Size		nbytes;
+	Size		nscanbytes;
+	Size		ninstrbytes;
 
 	Assert(snapshot != InvalidSnapshot);
 
 	RELATION_CHECKS;
 
-	nbytes = offsetof(ParallelIndexScanDescData, ps_snapshot_data);
-	nbytes = add_size(nbytes, EstimateSnapshotSpace(snapshot));
-	nbytes = MAXALIGN(nbytes);
+	nscanbytes = offsetof(ParallelIndexScanDescData, ps_snapshot_data);
+	nscanbytes = add_size(nscanbytes, EstimateSnapshotSpace(snapshot));
+	nscanbytes = MAXALIGN(nscanbytes);
 
 	/*
 	 * If amestimateparallelscan is not provided, assume there is no
@@ -469,11 +479,25 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	 * it's easy enough to cater to it here.)
 	 */
 	if (indexRelation->rd_indam->amestimateparallelscan != NULL)
-		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
-																		  norderbys));
+		nscanbytes = add_size(nscanbytes,
+							  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+																			  norderbys));
+	if (!instrument || nworkers == 0)
+	{
+		*instroffset = 0;		/* i.e. no instrumentation */
+		return nscanbytes;
+	}
 
-	return nbytes;
+	*instroffset = MAXALIGN(nscanbytes);	/* set *instroffset to start of
+											 * SharedIndexScanInstrumentation */
+
+	/* determine space required for instrumentation */
+	ninstrbytes = mul_size(nworkers, sizeof(IndexScanInstrumentation));
+	ninstrbytes = add_size(ninstrbytes,
+						   offsetof(SharedIndexScanInstrumentation, winstrument));
+	ninstrbytes = MAXALIGN(ninstrbytes);
+
+	return add_size(nscanbytes, ninstrbytes);
 }
 
 /*
@@ -488,21 +512,22 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
  */
 void
 index_parallelscan_initialize(Relation heapRelation, Relation indexRelation,
-							  Snapshot snapshot, ParallelIndexScanDesc target)
+							  Snapshot snapshot, ParallelIndexScanDesc target,
+							  Size ps_offset_ins)
 {
-	Size		offset;
+	Size		ps_offset_am;
 
 	Assert(snapshot != InvalidSnapshot);
 
 	RELATION_CHECKS;
 
-	offset = add_size(offsetof(ParallelIndexScanDescData, ps_snapshot_data),
-					  EstimateSnapshotSpace(snapshot));
-	offset = MAXALIGN(offset);
+	ps_offset_am = add_size(offsetof(ParallelIndexScanDescData, ps_snapshot_data),
+							EstimateSnapshotSpace(snapshot));
+	ps_offset_am = MAXALIGN(ps_offset_am);
 
 	target->ps_locator = heapRelation->rd_locator;
 	target->ps_indexlocator = indexRelation->rd_locator;
-	target->ps_offset = offset;
+	target->ps_offset_am = ps_offset_am;
 	SerializeSnapshot(snapshot, target->ps_snapshot_data);
 
 	/* aminitparallelscan is optional; assume no-op if not provided by AM */
@@ -510,9 +535,10 @@ index_parallelscan_initialize(Relation heapRelation, Relation indexRelation,
 	{
 		void	   *amtarget;
 
-		amtarget = OffsetToPointer(target, offset);
+		amtarget = OffsetToPointer(target, ps_offset_am);
 		indexRelation->rd_indam->aminitparallelscan(amtarget);
 	}
+	target->ps_offset_ins = ps_offset_ins;
 }
 
 /* ----------------
@@ -539,7 +565,8 @@ index_parallelrescan(IndexScanDesc scan)
  */
 IndexScanDesc
 index_beginscan_parallel(Relation heaprel, Relation indexrel, int nkeys,
-						 int norderbys, ParallelIndexScanDesc pscan)
+						 int norderbys, ParallelIndexScanDesc pscan,
+						 IndexScanInstrumentation *instrument)
 {
 	Snapshot	snapshot;
 	IndexScanDesc scan;
@@ -558,6 +585,7 @@ index_beginscan_parallel(Relation heaprel, Relation indexrel, int nkeys,
 	 */
 	scan->heapRelation = heaprel;
 	scan->xs_snapshot = snapshot;
+	scan->instrument = instrument;
 
 	/* prepare to fetch index matches from table */
 	scan->xs_heapfetch = table_index_fetch_begin(heaprel);
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 25188a644..c0a8833e0 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -574,7 +574,7 @@ btparallelrescan(IndexScanDesc scan)
 	Assert(parallel_scan);
 
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
-												  parallel_scan->ps_offset);
+												  parallel_scan->ps_offset_am);
 
 	/*
 	 * In theory, we don't need to acquire the LWLock here, because there
@@ -652,7 +652,7 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 	}
 
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
-												  parallel_scan->ps_offset);
+												  parallel_scan->ps_offset_am);
 
 	while (1)
 	{
@@ -760,7 +760,7 @@ _bt_parallel_release(IndexScanDesc scan, BlockNumber next_scan_page,
 	Assert(BlockNumberIsValid(next_scan_page));
 
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
-												  parallel_scan->ps_offset);
+												  parallel_scan->ps_offset_am);
 
 	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	btscan->btps_nextScanPage = next_scan_page;
@@ -799,7 +799,7 @@ _bt_parallel_done(IndexScanDesc scan)
 		return;
 
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
-												  parallel_scan->ps_offset);
+												  parallel_scan->ps_offset_am);
 
 	/*
 	 * Mark the parallel scan as done, unless some other process did so
@@ -837,7 +837,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 	Assert(so->numArrayKeys);
 
 	btscan = (BTParallelScanDesc) OffsetToPointer(parallel_scan,
-												  parallel_scan->ps_offset);
+												  parallel_scan->ps_offset_am);
 
 	LWLockAcquire(&btscan->btps_lock, LW_EXCLUSIVE);
 	if (btscan->btps_lastCurrPage == curr_page &&
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 6b2f464aa..22b27d01d 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -950,6 +950,8 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * _bt_search/_bt_endpoint below
 	 */
 	pgstat_count_index_scan(rel);
+	if (scan->instrument)
+		scan->instrument->nsearches++;
 
 	/*----------
 	 * Examine the scan keys to discover where we need to start the scan.
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 53f910e9d..25893050c 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -421,6 +421,8 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 
 	/* count an indexscan for stats */
 	pgstat_count_index_scan(scan->indexRelation);
+	if (scan->instrument)
+		scan->instrument->nsearches++;
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d8a7232ce..4b06275f5 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -125,6 +125,7 @@ static void show_recursive_union_info(RecursiveUnionState *rstate,
 static void show_memoize_info(MemoizeState *mstate, List *ancestors,
 							  ExplainState *es);
 static void show_hashagg_info(AggState *aggstate, ExplainState *es);
+static void show_indexsearches_info(PlanState *planstate, ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
 static void show_instrumentation_count(const char *qlabel, int which,
@@ -2096,6 +2097,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
+			show_indexsearches_info(planstate, es);
 			break;
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
@@ -2112,10 +2114,12 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (es->analyze)
 				ExplainPropertyFloat("Heap Fetches", NULL,
 									 planstate->instrument->ntuples2, 0, es);
+			show_indexsearches_info(planstate, es);
 			break;
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_indexsearches_info(planstate, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -3855,6 +3859,65 @@ show_hashagg_info(AggState *aggstate, ExplainState *es)
 	}
 }
 
+/*
+ * Show the total number of index searches performed by a
+ * IndexScan/IndexOnlyScan/BitmapIndexScan node
+ */
+static void
+show_indexsearches_info(PlanState *planstate, ExplainState *es)
+{
+	Plan	   *plan = planstate->plan;
+	SharedIndexScanInstrumentation *SharedInfo = NULL;
+	uint64		nsearches = 0;
+
+	if (!es->analyze)
+		return;
+
+	/* Initialize counters with stats from the local process first */
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			{
+				IndexScanState *indexstate = ((IndexScanState *) planstate);
+
+				nsearches = indexstate->iss_Instrument.nsearches;
+				SharedInfo = indexstate->iss_SharedInfo;
+				break;
+			}
+		case T_IndexOnlyScan:
+			{
+				IndexOnlyScanState *indexstate = ((IndexOnlyScanState *) planstate);
+
+				nsearches = indexstate->ioss_Instrument.nsearches;
+				SharedInfo = indexstate->ioss_SharedInfo;
+				break;
+			}
+		case T_BitmapIndexScan:
+			{
+				BitmapIndexScanState *indexstate = ((BitmapIndexScanState *) planstate);
+
+				nsearches = indexstate->biss_Instrument.nsearches;
+				SharedInfo = indexstate->biss_SharedInfo;
+				break;
+			}
+		default:
+			break;
+	}
+
+	/* Next get the sum of the counters set within each and every process */
+	if (SharedInfo)
+	{
+		for (int i = 0; i < SharedInfo->num_workers; ++i)
+		{
+			IndexScanInstrumentation *winstrument = &SharedInfo->winstrument[i];
+
+			nsearches += winstrument->nsearches;
+		}
+	}
+
+	ExplainPropertyUInteger("Index Searches", NULL, nsearches, es);
+}
+
 /*
  * Show exact/lossy pages for a BitmapHeapScan node
  */
diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c
index 742f3f8c0..e3fe9b78b 100644
--- a/src/backend/executor/execIndexing.c
+++ b/src/backend/executor/execIndexing.c
@@ -816,7 +816,7 @@ check_exclusion_or_unique_constraint(Relation heap, Relation index,
 retry:
 	conflict = false;
 	found_self = false;
-	index_scan = index_beginscan(heap, index, &DirtySnapshot, indnkeyatts, 0);
+	index_scan = index_beginscan(heap, index, &DirtySnapshot, NULL, indnkeyatts, 0);
 	index_rescan(index_scan, scankeys, indnkeyatts, NULL, 0);
 
 	while (index_getnext_slot(index_scan, ForwardScanDirection, existing_slot))
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index 1bedb8083..e9337a97d 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -28,6 +28,7 @@
 #include "executor/nodeAgg.h"
 #include "executor/nodeAppend.h"
 #include "executor/nodeBitmapHeapscan.h"
+#include "executor/nodeBitmapIndexscan.h"
 #include "executor/nodeCustom.h"
 #include "executor/nodeForeignscan.h"
 #include "executor/nodeHash.h"
@@ -244,14 +245,19 @@ ExecParallelEstimate(PlanState *planstate, ExecParallelEstimateContext *e)
 									e->pcxt);
 			break;
 		case T_IndexScanState:
-			if (planstate->plan->parallel_aware)
-				ExecIndexScanEstimate((IndexScanState *) planstate,
-									  e->pcxt);
+			/* even when not parallel-aware, for EXPLAIN ANALYZE */
+			ExecIndexScanEstimate((IndexScanState *) planstate,
+								  e->pcxt);
 			break;
 		case T_IndexOnlyScanState:
-			if (planstate->plan->parallel_aware)
-				ExecIndexOnlyScanEstimate((IndexOnlyScanState *) planstate,
-										  e->pcxt);
+			/* even when not parallel-aware, for EXPLAIN ANALYZE */
+			ExecIndexOnlyScanEstimate((IndexOnlyScanState *) planstate,
+									  e->pcxt);
+			break;
+		case T_BitmapIndexScanState:
+			/* even when not parallel-aware, for EXPLAIN ANALYZE */
+			ExecBitmapIndexScanEstimate((BitmapIndexScanState *) planstate,
+										e->pcxt);
 			break;
 		case T_ForeignScanState:
 			if (planstate->plan->parallel_aware)
@@ -468,14 +474,17 @@ ExecParallelInitializeDSM(PlanState *planstate,
 										 d->pcxt);
 			break;
 		case T_IndexScanState:
-			if (planstate->plan->parallel_aware)
-				ExecIndexScanInitializeDSM((IndexScanState *) planstate,
-										   d->pcxt);
+			/* even when not parallel-aware, for EXPLAIN ANALYZE */
+			ExecIndexScanInitializeDSM((IndexScanState *) planstate, d->pcxt);
 			break;
 		case T_IndexOnlyScanState:
-			if (planstate->plan->parallel_aware)
-				ExecIndexOnlyScanInitializeDSM((IndexOnlyScanState *) planstate,
-											   d->pcxt);
+			/* even when not parallel-aware, for EXPLAIN ANALYZE */
+			ExecIndexOnlyScanInitializeDSM((IndexOnlyScanState *) planstate,
+										   d->pcxt);
+			break;
+		case T_BitmapIndexScanState:
+			/* even when not parallel-aware, for EXPLAIN ANALYZE */
+			ExecBitmapIndexScanInitializeDSM((BitmapIndexScanState *) planstate, d->pcxt);
 			break;
 		case T_ForeignScanState:
 			if (planstate->plan->parallel_aware)
@@ -1002,6 +1011,7 @@ ExecParallelReInitializeDSM(PlanState *planstate,
 				ExecHashJoinReInitializeDSM((HashJoinState *) planstate,
 											pcxt);
 			break;
+		case T_BitmapIndexScanState:
 		case T_HashState:
 		case T_SortState:
 		case T_IncrementalSortState:
@@ -1063,6 +1073,15 @@ ExecParallelRetrieveInstrumentation(PlanState *planstate,
 	/* Perform any node-type-specific work that needs to be done. */
 	switch (nodeTag(planstate))
 	{
+		case T_IndexScanState:
+			ExecIndexScanRetrieveInstrumentation((IndexScanState *) planstate);
+			break;
+		case T_IndexOnlyScanState:
+			ExecIndexOnlyScanRetrieveInstrumentation((IndexOnlyScanState *) planstate);
+			break;
+		case T_BitmapIndexScanState:
+			ExecBitmapIndexScanRetrieveInstrumentation((BitmapIndexScanState *) planstate);
+			break;
 		case T_SortState:
 			ExecSortRetrieveInstrumentation((SortState *) planstate);
 			break;
@@ -1330,14 +1349,18 @@ ExecParallelInitializeWorker(PlanState *planstate, ParallelWorkerContext *pwcxt)
 				ExecSeqScanInitializeWorker((SeqScanState *) planstate, pwcxt);
 			break;
 		case T_IndexScanState:
-			if (planstate->plan->parallel_aware)
-				ExecIndexScanInitializeWorker((IndexScanState *) planstate,
-											  pwcxt);
+			/* even when not parallel-aware, for EXPLAIN ANALYZE */
+			ExecIndexScanInitializeWorker((IndexScanState *) planstate, pwcxt);
 			break;
 		case T_IndexOnlyScanState:
-			if (planstate->plan->parallel_aware)
-				ExecIndexOnlyScanInitializeWorker((IndexOnlyScanState *) planstate,
-												  pwcxt);
+			/* even when not parallel-aware, for EXPLAIN ANALYZE */
+			ExecIndexOnlyScanInitializeWorker((IndexOnlyScanState *) planstate,
+											  pwcxt);
+			break;
+		case T_BitmapIndexScanState:
+			/* even when not parallel-aware, for EXPLAIN ANALYZE */
+			ExecBitmapIndexScanInitializeWorker((BitmapIndexScanState *) planstate,
+												pwcxt);
 			break;
 		case T_ForeignScanState:
 			if (planstate->plan->parallel_aware)
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 5cef54f00..b52031b41 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -202,7 +202,7 @@ RelationFindReplTupleByIndex(Relation rel, Oid idxoid,
 	skey_attoff = build_replindex_scan_key(skey, rel, idxrel, searchslot);
 
 	/* Start an index scan. */
-	scan = index_beginscan(rel, idxrel, &snap, skey_attoff, 0);
+	scan = index_beginscan(rel, idxrel, &snap, NULL, skey_attoff, 0);
 
 retry:
 	found = false;
diff --git a/src/backend/executor/nodeBitmapIndexscan.c b/src/backend/executor/nodeBitmapIndexscan.c
index 0b32c3a02..e3e4dd36f 100644
--- a/src/backend/executor/nodeBitmapIndexscan.c
+++ b/src/backend/executor/nodeBitmapIndexscan.c
@@ -183,6 +183,21 @@ ExecEndBitmapIndexScan(BitmapIndexScanState *node)
 	indexRelationDesc = node->biss_RelationDesc;
 	indexScanDesc = node->biss_ScanDesc;
 
+	/*
+	 * When ending a parallel worker, copy the statistics gathered by the
+	 * worker back into shared memory so that it can be picked up by the main
+	 * process to report in EXPLAIN ANALYZE
+	 */
+	if (node->biss_SharedInfo != NULL && IsParallelWorker())
+	{
+		IndexScanInstrumentation *winstrument;
+
+		Assert(ParallelWorkerNumber <= node->biss_SharedInfo->num_workers);
+		winstrument = &node->biss_SharedInfo->winstrument[ParallelWorkerNumber];
+		memcpy(winstrument, &node->biss_Instrument,
+			   sizeof(IndexScanInstrumentation));
+	}
+
 	/*
 	 * close the index relation (no-op if we didn't open it)
 	 */
@@ -217,6 +232,7 @@ ExecInitBitmapIndexScan(BitmapIndexScan *node, EState *estate, int eflags)
 
 	/* normally we don't make the result bitmap till runtime */
 	indexstate->biss_result = NULL;
+	indexstate->biss_SharedInfo = NULL;
 
 	/*
 	 * We do not open or lock the base relation here.  We assume that an
@@ -302,6 +318,7 @@ ExecInitBitmapIndexScan(BitmapIndexScan *node, EState *estate, int eflags)
 	indexstate->biss_ScanDesc =
 		index_beginscan_bitmap(indexstate->biss_RelationDesc,
 							   estate->es_snapshot,
+							   &indexstate->biss_Instrument,
 							   indexstate->biss_NumScanKeys);
 
 	/*
@@ -319,3 +336,97 @@ ExecInitBitmapIndexScan(BitmapIndexScan *node, EState *estate, int eflags)
 	 */
 	return indexstate;
 }
+
+/* ----------------------------------------------------------------
+ *		ExecBitmapIndexScanEstimate
+ *
+ *		Compute the amount of space we'll need in the parallel
+ *		query DSM, and inform pcxt->estimator about our needs.
+ * ----------------------------------------------------------------
+ */
+void
+ExecBitmapIndexScanEstimate(BitmapIndexScanState *node, ParallelContext *pcxt)
+{
+	Size		size;
+
+	/*
+	 * Parallel bitmap index scans are not supported, but we still need to
+	 * store the scan's instrumentation in shared memory during parallel query
+	 */
+	if (!node->ss.ps.instrument || pcxt->nworkers == 0)
+		return;
+
+	size = mul_size(pcxt->nworkers, sizeof(IndexScanInstrumentation));
+	size = add_size(size, offsetof(SharedIndexScanInstrumentation, winstrument));
+	shm_toc_estimate_chunk(&pcxt->estimator, size);
+	shm_toc_estimate_keys(&pcxt->estimator, 1);
+}
+
+/* ----------------------------------------------------------------
+ *		ExecBitmapIndexScanInitializeDSM
+ *
+ *		Set up parallel bitmap index scan shared instrumentation.
+ * ----------------------------------------------------------------
+ */
+void
+ExecBitmapIndexScanInitializeDSM(BitmapIndexScanState *node,
+								 ParallelContext *pcxt)
+{
+	Size		size;
+
+	/* Only here to set up SharedInfo instrumentation */
+	if (!node->ss.ps.instrument || pcxt->nworkers == 0)
+		return;
+
+	size = offsetof(SharedIndexScanInstrumentation, winstrument) +
+		pcxt->nworkers * sizeof(IndexScanInstrumentation);
+	node->biss_SharedInfo =
+		(SharedIndexScanInstrumentation *) shm_toc_allocate(pcxt->toc,
+															size);
+	shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id,
+				   node->biss_SharedInfo);
+
+	/* Each per-worker area must start out as zeroes */
+	memset(node->biss_SharedInfo, 0, size);
+	node->biss_SharedInfo->num_workers = pcxt->nworkers;
+}
+
+/* ----------------------------------------------------------------
+ *		ExecBitmapIndexScanInitializeWorker
+ *
+ *		Copy relevant information from TOC into planstate.
+ * ----------------------------------------------------------------
+ */
+void
+ExecBitmapIndexScanInitializeWorker(BitmapIndexScanState *node,
+									ParallelWorkerContext *pwcxt)
+{
+	/* Only here to set up SharedInfo instrumentation */
+	if (!node->ss.ps.instrument)
+		return;
+
+	node->biss_SharedInfo = (SharedIndexScanInstrumentation *)
+		shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false);
+}
+
+/* ----------------------------------------------------------------
+ * ExecBitmapIndexScanRetrieveInstrumentation
+ *
+ *		Transfer bitmap index scan statistics from DSM to private memory.
+ * ----------------------------------------------------------------
+ */
+void
+ExecBitmapIndexScanRetrieveInstrumentation(BitmapIndexScanState *node)
+{
+	SharedIndexScanInstrumentation *SharedInfo = node->biss_SharedInfo;
+	size_t		size;
+
+	if (SharedInfo == NULL)
+		return;
+
+	/* Replace node->shared_info with a copy in backend-local memory */
+	size = offsetof(SharedIndexScanInstrumentation, winstrument) +
+		SharedInfo->num_workers * sizeof(IndexScanInstrumentation);
+	node->biss_SharedInfo = palloc(size);
+	memcpy(node->biss_SharedInfo, SharedInfo, size);
+}
diff --git a/src/backend/executor/nodeIndexonlyscan.c b/src/backend/executor/nodeIndexonlyscan.c
index e66352331..4f4229793 100644
--- a/src/backend/executor/nodeIndexonlyscan.c
+++ b/src/backend/executor/nodeIndexonlyscan.c
@@ -92,6 +92,7 @@ IndexOnlyNext(IndexOnlyScanState *node)
 		scandesc = index_beginscan(node->ss.ss_currentRelation,
 								   node->ioss_RelationDesc,
 								   estate->es_snapshot,
+								   &node->ioss_Instrument,
 								   node->ioss_NumScanKeys,
 								   node->ioss_NumOrderByKeys);
 
@@ -413,6 +414,21 @@ ExecEndIndexOnlyScan(IndexOnlyScanState *node)
 		node->ioss_VMBuffer = InvalidBuffer;
 	}
 
+	/*
+	 * When ending a parallel worker, copy the statistics gathered by the
+	 * worker back into shared memory so that it can be picked up by the main
+	 * process to report in EXPLAIN ANALYZE
+	 */
+	if (node->ioss_SharedInfo != NULL && IsParallelWorker())
+	{
+		IndexScanInstrumentation *winstrument;
+
+		Assert(ParallelWorkerNumber <= node->ioss_SharedInfo->num_workers);
+		winstrument = &node->ioss_SharedInfo->winstrument[ParallelWorkerNumber];
+		memcpy(winstrument, &node->ioss_Instrument,
+			   sizeof(IndexScanInstrumentation));
+	}
+
 	/*
 	 * close the index relation (no-op if we didn't open it)
 	 */
@@ -593,6 +609,7 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags)
 	indexstate->ioss_RuntimeKeysReady = false;
 	indexstate->ioss_RuntimeKeys = NULL;
 	indexstate->ioss_NumRuntimeKeys = 0;
+	indexstate->ioss_SharedInfo = NULL;
 
 	/*
 	 * build the index scan keys from the index qualification
@@ -711,7 +728,10 @@ ExecIndexOnlyScanEstimate(IndexOnlyScanState *node,
 	node->ioss_PscanLen = index_parallelscan_estimate(node->ioss_RelationDesc,
 													  node->ioss_NumScanKeys,
 													  node->ioss_NumOrderByKeys,
-													  estate->es_snapshot);
+													  estate->es_snapshot,
+													  pcxt->nworkers,
+													  node->ss.ps.instrument != NULL,
+													  &node->ioss_PscanInstrOffset);
 	shm_toc_estimate_chunk(&pcxt->estimator, node->ioss_PscanLen);
 	shm_toc_estimate_keys(&pcxt->estimator, 1);
 }
@@ -727,31 +747,49 @@ ExecIndexOnlyScanInitializeDSM(IndexOnlyScanState *node,
 							   ParallelContext *pcxt)
 {
 	EState	   *estate = node->ss.ps.state;
+	Size		size;
 	ParallelIndexScanDesc piscan;
 
 	piscan = shm_toc_allocate(pcxt->toc, node->ioss_PscanLen);
 	index_parallelscan_initialize(node->ss.ss_currentRelation,
 								  node->ioss_RelationDesc,
 								  estate->es_snapshot,
-								  piscan);
+								  piscan, node->ioss_PscanInstrOffset);
 	shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id, piscan);
-	node->ioss_ScanDesc =
-		index_beginscan_parallel(node->ss.ss_currentRelation,
-								 node->ioss_RelationDesc,
-								 node->ioss_NumScanKeys,
-								 node->ioss_NumOrderByKeys,
-								 piscan);
-	node->ioss_ScanDesc->xs_want_itup = true;
-	node->ioss_VMBuffer = InvalidBuffer;
+	if (node->ss.ps.plan->parallel_aware)
+	{
+		node->ioss_ScanDesc =
+			index_beginscan_parallel(node->ss.ss_currentRelation,
+									 node->ioss_RelationDesc,
+									 node->ioss_NumScanKeys,
+									 node->ioss_NumOrderByKeys,
+									 piscan,
+									 &node->ioss_Instrument);
+		node->ioss_ScanDesc->xs_want_itup = true;
+		node->ioss_VMBuffer = InvalidBuffer;
 
-	/*
-	 * If no run-time keys to calculate or they are ready, go ahead and pass
-	 * the scankeys to the index AM.
-	 */
-	if (node->ioss_NumRuntimeKeys == 0 || node->ioss_RuntimeKeysReady)
-		index_rescan(node->ioss_ScanDesc,
-					 node->ioss_ScanKeys, node->ioss_NumScanKeys,
-					 node->ioss_OrderByKeys, node->ioss_NumOrderByKeys);
+		/*
+		 * If no run-time keys to calculate or they are ready, go ahead and
+		 * pass the scankeys to the index AM.
+		 */
+		if (node->ioss_NumRuntimeKeys == 0 || node->ioss_RuntimeKeysReady)
+			index_rescan(node->ioss_ScanDesc,
+						 node->ioss_ScanKeys, node->ioss_NumScanKeys,
+						 node->ioss_OrderByKeys, node->ioss_NumOrderByKeys);
+	}
+
+	/* Done if SharedInfo instrumentation space isn't required */
+	if (node->ioss_PscanInstrOffset == 0)
+		return;
+
+	size = offsetof(SharedIndexScanInstrumentation, winstrument) +
+		pcxt->nworkers * sizeof(IndexScanInstrumentation);
+	node->ioss_SharedInfo = (SharedIndexScanInstrumentation *)
+		OffsetToPointer(piscan, piscan->ps_offset_ins);
+
+	/* Each per-worker area must start out as zeroes */
+	memset(node->ioss_SharedInfo, 0, size);
+	node->ioss_SharedInfo->num_workers = pcxt->nworkers;
 }
 
 /* ----------------------------------------------------------------
@@ -764,6 +802,7 @@ void
 ExecIndexOnlyScanReInitializeDSM(IndexOnlyScanState *node,
 								 ParallelContext *pcxt)
 {
+	Assert(node->ss.ps.plan->parallel_aware);
 	index_parallelrescan(node->ioss_ScanDesc);
 }
 
@@ -780,20 +819,53 @@ ExecIndexOnlyScanInitializeWorker(IndexOnlyScanState *node,
 	ParallelIndexScanDesc piscan;
 
 	piscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false);
-	node->ioss_ScanDesc =
-		index_beginscan_parallel(node->ss.ss_currentRelation,
-								 node->ioss_RelationDesc,
-								 node->ioss_NumScanKeys,
-								 node->ioss_NumOrderByKeys,
-								 piscan);
-	node->ioss_ScanDesc->xs_want_itup = true;
+	if (node->ss.ps.plan->parallel_aware)
+	{
+		node->ioss_ScanDesc =
+			index_beginscan_parallel(node->ss.ss_currentRelation,
+									 node->ioss_RelationDesc,
+									 node->ioss_NumScanKeys,
+									 node->ioss_NumOrderByKeys,
+									 piscan,
+									 &node->ioss_Instrument);
+		node->ioss_ScanDesc->xs_want_itup = true;
 
-	/*
-	 * If no run-time keys to calculate or they are ready, go ahead and pass
-	 * the scankeys to the index AM.
-	 */
-	if (node->ioss_NumRuntimeKeys == 0 || node->ioss_RuntimeKeysReady)
-		index_rescan(node->ioss_ScanDesc,
-					 node->ioss_ScanKeys, node->ioss_NumScanKeys,
-					 node->ioss_OrderByKeys, node->ioss_NumOrderByKeys);
+		/*
+		 * If no run-time keys to calculate or they are ready, go ahead and
+		 * pass the scankeys to the index AM.
+		 */
+		if (node->ioss_NumRuntimeKeys == 0 || node->ioss_RuntimeKeysReady)
+			index_rescan(node->ioss_ScanDesc,
+						 node->ioss_ScanKeys, node->ioss_NumScanKeys,
+						 node->ioss_OrderByKeys, node->ioss_NumOrderByKeys);
+	}
+
+	/* Done if SharedInfo instrumentation space isn't required */
+	if (piscan->ps_offset_ins == 0)
+		return;
+
+	node->ioss_SharedInfo = (SharedIndexScanInstrumentation *)
+		OffsetToPointer(piscan, piscan->ps_offset_ins);
+}
+
+/* ----------------------------------------------------------------
+ *		ExecIndexOnlyScanRetrieveInstrumentation
+ *
+ *		Transfer index-only scan statistics from DSM to private memory.
+ * ----------------------------------------------------------------
+ */
+void
+ExecIndexOnlyScanRetrieveInstrumentation(IndexOnlyScanState *node)
+{
+	SharedIndexScanInstrumentation *SharedInfo = node->ioss_SharedInfo;
+	size_t		size;
+
+	if (SharedInfo == NULL)
+		return;
+
+	/* Replace node->shared_info with a copy in backend-local memory */
+	size = offsetof(SharedIndexScanInstrumentation, winstrument) +
+		SharedInfo->num_workers * sizeof(IndexScanInstrumentation);
+	node->ioss_SharedInfo = palloc(size);
+	memcpy(node->ioss_SharedInfo, SharedInfo, size);
 }
diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c
index c30b9c2c1..fdc3304cf 100644
--- a/src/backend/executor/nodeIndexscan.c
+++ b/src/backend/executor/nodeIndexscan.c
@@ -109,6 +109,7 @@ IndexNext(IndexScanState *node)
 		scandesc = index_beginscan(node->ss.ss_currentRelation,
 								   node->iss_RelationDesc,
 								   estate->es_snapshot,
+								   &node->iss_Instrument,
 								   node->iss_NumScanKeys,
 								   node->iss_NumOrderByKeys);
 
@@ -204,6 +205,7 @@ IndexNextWithReorder(IndexScanState *node)
 		scandesc = index_beginscan(node->ss.ss_currentRelation,
 								   node->iss_RelationDesc,
 								   estate->es_snapshot,
+								   &node->iss_Instrument,
 								   node->iss_NumScanKeys,
 								   node->iss_NumOrderByKeys);
 
@@ -793,6 +795,21 @@ ExecEndIndexScan(IndexScanState *node)
 	indexRelationDesc = node->iss_RelationDesc;
 	indexScanDesc = node->iss_ScanDesc;
 
+	/*
+	 * When ending a parallel worker, copy the statistics gathered by the
+	 * worker back into shared memory so that it can be picked up by the main
+	 * process to report in EXPLAIN ANALYZE
+	 */
+	if (node->iss_SharedInfo != NULL && IsParallelWorker())
+	{
+		IndexScanInstrumentation *winstrument;
+
+		Assert(ParallelWorkerNumber <= node->iss_SharedInfo->num_workers);
+		winstrument = &node->iss_SharedInfo->winstrument[ParallelWorkerNumber];
+		memcpy(winstrument, &node->iss_Instrument,
+			   sizeof(IndexScanInstrumentation));
+	}
+
 	/*
 	 * close the index relation (no-op if we didn't open it)
 	 */
@@ -960,6 +977,7 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags)
 	indexstate->iss_RuntimeKeysReady = false;
 	indexstate->iss_RuntimeKeys = NULL;
 	indexstate->iss_NumRuntimeKeys = 0;
+	indexstate->iss_SharedInfo = NULL;
 
 	/*
 	 * build the index scan keys from the index qualification
@@ -1646,7 +1664,10 @@ ExecIndexScanEstimate(IndexScanState *node,
 	node->iss_PscanLen = index_parallelscan_estimate(node->iss_RelationDesc,
 													 node->iss_NumScanKeys,
 													 node->iss_NumOrderByKeys,
-													 estate->es_snapshot);
+													 estate->es_snapshot,
+													 pcxt->nworkers,
+													 node->ss.ps.instrument != NULL,
+													 &node->iss_PscanInstrOffset);
 	shm_toc_estimate_chunk(&pcxt->estimator, node->iss_PscanLen);
 	shm_toc_estimate_keys(&pcxt->estimator, 1);
 }
@@ -1662,29 +1683,48 @@ ExecIndexScanInitializeDSM(IndexScanState *node,
 						   ParallelContext *pcxt)
 {
 	EState	   *estate = node->ss.ps.state;
+	Size		size;
 	ParallelIndexScanDesc piscan;
 
 	piscan = shm_toc_allocate(pcxt->toc, node->iss_PscanLen);
 	index_parallelscan_initialize(node->ss.ss_currentRelation,
 								  node->iss_RelationDesc,
 								  estate->es_snapshot,
-								  piscan);
-	shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id, piscan);
-	node->iss_ScanDesc =
-		index_beginscan_parallel(node->ss.ss_currentRelation,
-								 node->iss_RelationDesc,
-								 node->iss_NumScanKeys,
-								 node->iss_NumOrderByKeys,
-								 piscan);
+								  piscan, node->iss_PscanInstrOffset);
 
-	/*
-	 * If no run-time keys to calculate or they are ready, go ahead and pass
-	 * the scankeys to the index AM.
-	 */
-	if (node->iss_NumRuntimeKeys == 0 || node->iss_RuntimeKeysReady)
-		index_rescan(node->iss_ScanDesc,
-					 node->iss_ScanKeys, node->iss_NumScanKeys,
-					 node->iss_OrderByKeys, node->iss_NumOrderByKeys);
+	shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id, piscan);
+	if (node->ss.ps.plan->parallel_aware)
+	{
+		node->iss_ScanDesc =
+			index_beginscan_parallel(node->ss.ss_currentRelation,
+									 node->iss_RelationDesc,
+									 node->iss_NumScanKeys,
+									 node->iss_NumOrderByKeys,
+									 piscan,
+									 &node->iss_Instrument);
+
+		/*
+		 * If no run-time keys to calculate or they are ready, go ahead and
+		 * pass the scankeys to the index AM.
+		 */
+		if (node->iss_NumRuntimeKeys == 0 || node->iss_RuntimeKeysReady)
+			index_rescan(node->iss_ScanDesc,
+						 node->iss_ScanKeys, node->iss_NumScanKeys,
+						 node->iss_OrderByKeys, node->iss_NumOrderByKeys);
+	}
+
+	/* Done if shared memory contains no instrumentation state */
+	if (node->iss_PscanInstrOffset == 0)
+		return;
+
+	size = offsetof(SharedIndexScanInstrumentation, winstrument) +
+		pcxt->nworkers * sizeof(IndexScanInstrumentation);
+	node->iss_SharedInfo = (SharedIndexScanInstrumentation *)
+		OffsetToPointer(piscan, piscan->ps_offset_ins);
+
+	/* Each per-worker area must start out as zeroes */
+	memset(node->iss_SharedInfo, 0, size);
+	node->iss_SharedInfo->num_workers = pcxt->nworkers;
 }
 
 /* ----------------------------------------------------------------
@@ -1697,6 +1737,7 @@ void
 ExecIndexScanReInitializeDSM(IndexScanState *node,
 							 ParallelContext *pcxt)
 {
+	Assert(node->ss.ps.plan->parallel_aware);
 	index_parallelrescan(node->iss_ScanDesc);
 }
 
@@ -1713,19 +1754,52 @@ ExecIndexScanInitializeWorker(IndexScanState *node,
 	ParallelIndexScanDesc piscan;
 
 	piscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false);
-	node->iss_ScanDesc =
-		index_beginscan_parallel(node->ss.ss_currentRelation,
-								 node->iss_RelationDesc,
-								 node->iss_NumScanKeys,
-								 node->iss_NumOrderByKeys,
-								 piscan);
+	if (node->ss.ps.plan->parallel_aware)
+	{
+		node->iss_ScanDesc =
+			index_beginscan_parallel(node->ss.ss_currentRelation,
+									 node->iss_RelationDesc,
+									 node->iss_NumScanKeys,
+									 node->iss_NumOrderByKeys,
+									 piscan,
+									 &node->iss_Instrument);
 
-	/*
-	 * If no run-time keys to calculate or they are ready, go ahead and pass
-	 * the scankeys to the index AM.
-	 */
-	if (node->iss_NumRuntimeKeys == 0 || node->iss_RuntimeKeysReady)
-		index_rescan(node->iss_ScanDesc,
-					 node->iss_ScanKeys, node->iss_NumScanKeys,
-					 node->iss_OrderByKeys, node->iss_NumOrderByKeys);
+		/*
+		 * If no run-time keys to calculate or they are ready, go ahead and
+		 * pass the scankeys to the index AM.
+		 */
+		if (node->iss_NumRuntimeKeys == 0 || node->iss_RuntimeKeysReady)
+			index_rescan(node->iss_ScanDesc,
+						 node->iss_ScanKeys, node->iss_NumScanKeys,
+						 node->iss_OrderByKeys, node->iss_NumOrderByKeys);
+	}
+
+	/* Done if shared memory contains no instrumentation state */
+	if (piscan->ps_offset_ins == 0)
+		return;
+
+	node->iss_SharedInfo = (SharedIndexScanInstrumentation *)
+		OffsetToPointer(piscan, piscan->ps_offset_ins);
+}
+
+/* ----------------------------------------------------------------
+ * ExecIndexScanRetrieveInstrumentation
+ *
+ *		Transfer index scan statistics from DSM to private memory.
+ * ----------------------------------------------------------------
+ */
+void
+ExecIndexScanRetrieveInstrumentation(IndexScanState *node)
+{
+	SharedIndexScanInstrumentation *SharedInfo = node->iss_SharedInfo;
+	size_t		size;
+
+	if (SharedInfo == NULL)
+		return;
+
+	/* Replace node->shared_info with a copy in backend-local memory */
+	size = offsetof(SharedIndexScanInstrumentation, winstrument) +
+		SharedInfo->num_workers * sizeof(IndexScanInstrumentation);
+	node->iss_SharedInfo = palloc(size);
+	memcpy(node->iss_SharedInfo, SharedInfo, size);
 }
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index c2918c9c8..b4dc91c7c 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -6376,7 +6376,7 @@ get_actual_variable_endpoint(Relation heapRel,
 							  GlobalVisTestFor(heapRel));
 
 	index_scan = index_beginscan(heapRel, indexRel,
-								 &SnapshotNonVacuumable,
+								 &SnapshotNonVacuumable, NULL,
 								 1, 0);
 	/* Set it up for index-only scan */
 	index_scan->xs_want_itup = true;
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index bf801fe78..d072f47fe 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -116,6 +116,8 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
+	if (scan->instrument)
+		scan->instrument->nsearches++;
 
 	for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++)
 	{
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 663a0a4a6..ec5d07767 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -173,10 +173,11 @@ CREATE INDEX
    Buffers: shared hit=21864
    -&gt;  Bitmap Index Scan on bloomidx  (cost=0.00..178436.00 rows=1 width=0) (actual time=20.005..20.005 rows=2300.00 loops=1)
          Index Cond: ((i2 = 898732) AND (i5 = 123451))
+         Index Searches: 1
          Buffers: shared hit=19608
  Planning Time: 0.099 ms
  Execution Time: 22.632 ms
-(10 rows)
+(11 rows)
 </programlisting>
   </para>
 
@@ -208,13 +209,15 @@ CREATE INDEX
          Buffers: shared hit=6
          -&gt;  Bitmap Index Scan on btreeidx5  (cost=0.00..4.52 rows=11 width=0) (actual time=0.026..0.026 rows=7.00 loops=1)
                Index Cond: (i5 = 123451)
+               Index Searches: 1
                Buffers: shared hit=3
          -&gt;  Bitmap Index Scan on btreeidx2  (cost=0.00..4.52 rows=11 width=0) (actual time=0.007..0.007 rows=8.00 loops=1)
                Index Cond: (i2 = 898732)
+               Index Searches: 1
                Buffers: shared hit=3
  Planning Time: 0.264 ms
  Execution Time: 0.047 ms
-(13 rows)
+(15 rows)
 </programlisting>
    Although this query runs much faster than with either of the single
    indexes, we pay a penalty in index size.  Each of the single-column
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 16646f560..fd9bdd884 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4234,16 +4234,32 @@ description | Waiting for a newly initialized WAL file to reach durable storage
 
   <note>
    <para>
-    Queries that use certain <acronym>SQL</acronym> constructs to search for
-    rows matching any value out of a list or array of multiple scalar values
-    (see <xref linkend="functions-comparisons"/>) perform multiple
-    <quote>primitive</quote> index scans (up to one primitive scan per scalar
-    value) during query execution.  Each internal primitive index scan
-    increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
+    Index scans may sometimes perform multiple index searches per execution.
+    Each index search increments <structname>pg_stat_all_indexes</structname>.<structfield>idx_scan</structfield>,
     so it's possible for the count of index scans to significantly exceed the
     total number of index scan executor node executions.
    </para>
+   <para>
+    This can happen with queries that use certain <acronym>SQL</acronym>
+    constructs to search for rows matching any value out of a list or array of
+    multiple scalar values (see <xref linkend="functions-comparisons"/>).  It
+    can also happen to queries with a
+    <literal><replaceable>column_name</replaceable> =
+     <replaceable>value1</replaceable> OR
+     <replaceable>column_name</replaceable> =
+     <replaceable>value2</replaceable> ...</literal> construct, though only
+    when the optimizer transforms the construct into an equivalent
+    multi-valued array representation.
+   </para>
   </note>
+  <tip>
+   <para>
+    <command>EXPLAIN ANALYZE</command> outputs the total number of index
+    searches performed by each index scan node.  See
+    <xref linkend="using-explain-analyze"/> for an example demonstrating how
+    this works.
+   </para>
+  </tip>
 
  </sect2>
 
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index 91feb59ab..b4bb03253 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -729,9 +729,11 @@ WHERE t1.unique1 &lt; 10 AND t1.unique2 = t2.unique2;
          Buffers: shared hit=3 read=5 written=4
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10.00 loops=1)
                Index Cond: (unique1 &lt; 10)
+               Index Searches: 1
                Buffers: shared hit=2
    -&gt;  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1.00 loops=10)
          Index Cond: (unique2 = t1.unique2)
+         Index Searches: 10
          Buffers: shared hit=24 read=6
  Planning:
    Buffers: shared hit=15 dirtied=9
@@ -790,6 +792,7 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
                      Buffers: shared hit=92
                      -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100.00 loops=1)
                            Index Cond: (unique1 &lt; 100)
+                           Index Searches: 1
                            Buffers: shared hit=2
  Planning:
    Buffers: shared hit=12
@@ -805,6 +808,58 @@ WHERE t1.unique1 &lt; 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
     shown.)
    </para>
 
+   <para>
+    Index Scan nodes (as well as Bitmap Index Scan and Index-Only Scan nodes)
+    show an <quote>Index Searches</quote> line that reports the total number
+    of searches across <emphasis>all</emphasis> node
+    executions/<literal>loops</literal>:
+
+<screen>
+EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE thousand IN (1, 500, 700, 999);
+                                                            QUERY PLAN
+-------------------------------------------------------------------&zwsp;---------------------------------------------------------
+ Bitmap Heap Scan on tenk1  (cost=9.45..73.44 rows=40 width=244) (actual time=0.012..0.028 rows=40.00 loops=1)
+   Recheck Cond: (thousand = ANY ('{1,500,700,999}'::integer[]))
+   Heap Blocks: exact=39
+   Buffers: shared hit=47
+   ->  Bitmap Index Scan on tenk1_thous_tenthous  (cost=0.00..9.44 rows=40 width=0) (actual time=0.009..0.009 rows=40.00 loops=1)
+         Index Cond: (thousand = ANY ('{1,500,700,999}'::integer[]))
+         Index Searches: 4
+         Buffers: shared hit=8
+ Planning Time: 0.037 ms
+ Execution Time: 0.034 ms
+</screen>
+
+    Here we see a Bitmap Index Scan node that needed 4 separate index
+    searches.  The scan had to search the index from the
+    <structname>tenk1_thous_tenthous</structname> index root page once per
+    <type>integer</type> value from the predicate's <literal>IN</literal>
+    construct.  However, the number of index searches often won't have such a
+    simple correspondence to the query predicate:
+
+<screen>
+EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE thousand IN (1, 2, 3, 4);
+                                                            QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------
+ Bitmap Heap Scan on tenk1  (cost=9.45..73.44 rows=40 width=244) (actual time=0.009..0.019 rows=40.00 loops=1)
+   Recheck Cond: (thousand = ANY ('{1,2,3,4}'::integer[]))
+   Heap Blocks: exact=38
+   Buffers: shared hit=40
+   ->  Bitmap Index Scan on tenk1_thous_tenthous  (cost=0.00..9.44 rows=40 width=0) (actual time=0.005..0.005 rows=40.00 loops=1)
+         Index Cond: (thousand = ANY ('{1,2,3,4}'::integer[]))
+         Index Searches: 1
+         Buffers: shared hit=2
+ Planning Time: 0.029 ms
+ Execution Time: 0.026 ms
+</screen>
+
+    This variant of our <literal>IN</literal> query performed only 1 index
+    search.  It spent less time traversing the index (compared to the original
+    query) because its <literal>IN</literal> construct uses values matching
+    index tuples stored next to each other, on the same
+    <structname>tenk1_thous_tenthous</structname> index leaf page.
+   </para>
+
    <para>
     Another type of extra information is the number of rows removed by a
     filter condition:
@@ -861,6 +916,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @&gt; polygon '(0.5,2.0)';
  Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0.00 loops=1)
    Index Cond: (f1 @&gt; '((0.5,2))'::polygon)
    Rows Removed by Index Recheck: 1
+   Index Searches: 1
    Buffers: shared hit=1
  Planning Time: 0.039 ms
  Execution Time: 0.098 ms
@@ -894,8 +950,10 @@ EXPLAIN (ANALYZE, BUFFERS OFF) SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND un
    -&gt;  BitmapAnd  (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0.00 loops=1)
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100.00 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
          -&gt;  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999.00 loops=1)
                Index Cond: (unique2 &gt; 9000)
+               Index Searches: 1
  Planning Time: 0.162 ms
  Execution Time: 0.143 ms
 </screen>
@@ -923,6 +981,7 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 &lt; 100;
          Buffers: shared hit=4 read=2
          -&gt;  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100.00 loops=1)
                Index Cond: (unique1 &lt; 100)
+               Index Searches: 1
                Buffers: shared read=2
  Planning Time: 0.151 ms
  Execution Time: 1.856 ms
@@ -1061,6 +1120,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
          Index Cond: (unique2 &gt; 9000)
          Filter: (unique1 &lt; 100)
          Rows Removed by Filter: 287
+         Index Searches: 1
          Buffers: shared hit=16
  Planning Time: 0.077 ms
  Execution Time: 0.086 ms
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index 7daddf03e..9ed1061b7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -506,10 +506,11 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
    Buffers: shared hit=4
    -&gt;  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99.00 loops=1)
          Index Cond: ((id &gt; 100) AND (id &lt; 200))
+         Index Searches: 1
          Buffers: shared hit=4
  Planning Time: 0.244 ms
  Execution Time: 0.073 ms
-(9 rows)
+(10 rows)
 </programlisting>
   </para>
 
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 1d9924a2a..8467d961f 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1046,6 +1046,7 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
    -&gt;  Index Only Scan using wrd_word on wrd  (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0.00 loops=1)
          Index Cond: (word = 'caterpiler'::text)
          Heap Fetches: 0
+         Index Searches: 1
  Planning time: 0.164 ms
  Execution time: 0.117 ms
 </programlisting>
@@ -1090,6 +1091,7 @@ SELECT word FROM words ORDER BY word &lt;-&gt; 'caterpiler' LIMIT 10;
  Limit  (cost=0.29..1.06 rows=10 width=10) (actual time=187.222..188.257 rows=10.00 loops=1)
    -&gt;  Index Scan using wrd_trgm on wrd  (cost=0.29..37020.87 rows=479829 width=10) (actual time=187.219..188.252 rows=10.00 loops=1)
          Order By: (word &lt;-&gt; 'caterpiler'::text)
+         Index Searches: 1
  Planning time: 0.196 ms
  Execution time: 198.640 ms
 </programlisting>
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index 991b7eaca..cb5b5e53e 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -853,7 +853,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -872,7 +873,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
    Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
@@ -882,7 +884,8 @@ SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
    Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
    ->  Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_timestamp_test;
 RESET enable_seqscan;
@@ -900,7 +903,8 @@ SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
    Recheck Cond: (a = '2023-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '2023-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
@@ -910,7 +914,8 @@ SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
    Recheck Cond: (a = '1900-01-01'::date)
    ->  Bitmap Index Scan on brin_date_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '1900-01-01'::date)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_date_test;
 RESET enable_seqscan;
@@ -929,7 +934,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -939,7 +945,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
@@ -957,7 +964,8 @@ SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
    Recheck Cond: (a = '@ 30 years ago'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '@ 30 years ago'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
 SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
@@ -967,7 +975,8 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
    Recheck Cond: (a = '@ 30 years'::interval)
    ->  Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0.00 loops=1)
          Index Cond: (a = '@ 30 years'::interval)
-(4 rows)
+         Index Searches: 1
+(5 rows)
 
 DROP TABLE brin_interval_test;
 RESET enable_seqscan;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 22f2d3284..38dfaf021 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -22,8 +22,9 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
@@ -49,7 +50,8 @@ WHERE t2.unique1 < 1000;', false);
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1.00 loops=N)
                      Index Cond: (unique1 = t2.twenty)
                      Heap Fetches: N
-(12 rows)
+                     Index Searches: N
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
@@ -80,7 +82,8 @@ WHERE t1.unique1 < 1000;', false);
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1.00 loops=N)
                      Index Cond: (unique1 = t1.twenty)
                      Heap Fetches: N
-(12 rows)
+                     Index Searches: N
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
@@ -106,6 +109,7 @@ WHERE t1.unique1 < 10;', false);
    ->  Nested Loop Left Join (actual rows=20.00 loops=N)
          ->  Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10.00 loops=N)
                Index Cond: (unique1 < 10)
+               Index Searches: N
          ->  Memoize (actual rows=2.00 loops=N)
                Cache Key: t1.two
                Cache Mode: binary
@@ -115,7 +119,8 @@ WHERE t1.unique1 < 10;', false);
                      Rows Removed by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4.00 loops=N)
                            Index Cond: (unique1 < 4)
-(13 rows)
+                           Index Searches: N
+(15 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*),AVG(t2.t1two) FROM tenk1 t1 LEFT JOIN
@@ -149,7 +154,8 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
                      Filter: ((t1.two + 1) = unique1)
                      Rows Removed by Filter: 9999
                      Heap Fetches: N
-(13 rows)
+                     Index Searches: N
+(14 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -219,7 +225,8 @@ ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
                Index Cond: (x = (t1.t)::numeric)
                Filter: (t1.x = (t)::numeric)
                Heap Fetches: N
-(10 rows)
+               Index Searches: N
+(11 rows)
 
 DROP TABLE expr_key;
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
@@ -246,7 +253,8 @@ WHERE t2.unique1 < 1200;', true);
                ->  Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1.00 loops=N)
                      Index Cond: (unique1 = t2.thousand)
                      Heap Fetches: N
-(12 rows)
+                     Index Searches: N
+(13 rows)
 
 CREATE TABLE flt (f float);
 CREATE INDEX flt_f_idx ON flt (f);
@@ -261,6 +269,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
  Nested Loop (actual rows=4.00 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2.00 loops=N)
          Heap Fetches: N
+         Index Searches: N
    ->  Memoize (actual rows=2.00 loops=N)
          Cache Key: f1.f
          Cache Mode: logical
@@ -268,7 +277,8 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2.00 loops=N)
                Index Cond: (f = f1.f)
                Heap Fetches: N
-(10 rows)
+               Index Searches: N
+(12 rows)
 
 -- Ensure memoize operates in binary mode
 SELECT explain_memoize('
@@ -278,6 +288,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
  Nested Loop (actual rows=4.00 loops=N)
    ->  Index Only Scan using flt_f_idx on flt f1 (actual rows=2.00 loops=N)
          Heap Fetches: N
+         Index Searches: N
    ->  Memoize (actual rows=2.00 loops=N)
          Cache Key: f1.f
          Cache Mode: binary
@@ -285,7 +296,8 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
          ->  Index Only Scan using flt_f_idx on flt f2 (actual rows=2.00 loops=N)
                Index Cond: (f <= f1.f)
                Heap Fetches: N
-(10 rows)
+               Index Searches: N
+(12 rows)
 
 DROP TABLE flt;
 -- Exercise Memoize in binary mode with a large fixed width type and a
@@ -311,7 +323,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_n_idx on strtest s2 (actual rows=4.00 loops=N)
                Index Cond: (n <= s1.n)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 -- Ensure we get 3 hits and 3 misses
 SELECT explain_memoize('
@@ -327,7 +340,8 @@ SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
          Hits: 3  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
          ->  Index Scan using strtest_t_idx on strtest s2 (actual rows=4.00 loops=N)
                Index Cond: (t <= s1.t)
-(9 rows)
+               Index Searches: N
+(10 rows)
 
 DROP TABLE strtest;
 -- Ensure memoize works with partitionwise join
@@ -348,6 +362,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
    ->  Nested Loop (actual rows=16.00 loops=N)
          ->  Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4.00 loops=N)
                Heap Fetches: N
+               Index Searches: N
          ->  Memoize (actual rows=4.00 loops=N)
                Cache Key: t1_1.a
                Cache Mode: logical
@@ -355,9 +370,11 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                ->  Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4.00 loops=N)
                      Index Cond: (a = t1_1.a)
                      Heap Fetches: N
+                     Index Searches: N
    ->  Nested Loop (actual rows=16.00 loops=N)
          ->  Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4.00 loops=N)
                Heap Fetches: N
+               Index Searches: N
          ->  Memoize (actual rows=4.00 loops=N)
                Cache Key: t1_2.a
                Cache Mode: logical
@@ -365,7 +382,8 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
                ->  Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4.00 loops=N)
                      Index Cond: (a = t1_2.a)
                      Heap Fetches: N
-(21 rows)
+                     Index Searches: N
+(25 rows)
 
 -- Ensure memoize works with parameterized union-all Append path
 SET enable_partitionwise_join TO off;
@@ -378,6 +396,7 @@ ON t1.a = t2.a;', false);
  Nested Loop (actual rows=16.00 loops=N)
    ->  Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4.00 loops=N)
          Heap Fetches: N
+         Index Searches: N
    ->  Memoize (actual rows=4.00 loops=N)
          Cache Key: t1.a
          Cache Mode: logical
@@ -386,10 +405,12 @@ ON t1.a = t2.a;', false);
                ->  Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4.00 loops=N)
                      Index Cond: (a = t1.a)
                      Heap Fetches: N
+                     Index Searches: N
                ->  Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0.00 loops=N)
                      Index Cond: (a = t1.a)
                      Heap Fetches: N
-(14 rows)
+                     Index Searches: N
+(17 rows)
 
 DROP TABLE prt;
 RESET enable_partitionwise_join;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index d95d2395d..34f2b0b8d 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2369,6 +2369,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
@@ -2686,47 +2690,56 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0.00 loops=1)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0.00 loops=1)
                Index Cond: (a = (InitPlan 1).col1)
+               Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
          Recheck Cond: (a = (InitPlan 1).col1)
          Filter: (b = (InitPlan 2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
                Index Cond: (a = (InitPlan 1).col1)
-(52 rows)
+               Index Searches: 0
+(61 rows)
 
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2742,16 +2755,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2770,7 +2786,7 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(37 rows)
+(40 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -2786,16 +2802,19 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1)
                      Index Cond: (a = 1)
+                     Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
                Filter: (b = (InitPlan 1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
+                     Index Searches: 0
    ->  Result (actual rows=0.00 loops=1)
          One-Time Filter: (5 = (InitPlan 1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
@@ -2816,7 +2835,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = (InitPlan 1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = (InitPlan 1).col1)
-(39 rows)
+(42 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2887,16 +2906,19 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1.00 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1.00 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0.00 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1.00 loops=1)
                            Index Cond: (a = 1)
+                           Index Searches: 1
          ->  Materialize (actual rows=1.00 loops=1)
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1.00 loops=1)
@@ -2904,17 +2926,20 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
                            Recheck Cond: (a = 1)
                            ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1.00 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1.00 loops=1)
                                  Index Cond: (a = 1)
+                                 Index Searches: 1
                      ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0.00 loops=1)
                            Recheck Cond: (a = 1)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1.00 loops=1)
                                  Index Cond: (a = 1)
-(37 rows)
+                                 Index Searches: 1
+(43 rows)
 
 table ab;
  a | b 
@@ -2990,17 +3015,23 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=3.00 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2.00 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2.00 loops=1)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 1
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
@@ -3011,17 +3042,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=1.00 loops=2)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1.00 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3056,17 +3093,23 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
    ->  Append (actual rows=4.60 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (actual rows=2.00 loops=5)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 5
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=2.75 loops=4)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 4
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=1.00 loops=2)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 < tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 < tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
@@ -3077,17 +3120,23 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0.60 loops=5)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (actual rows=1.00 loops=2)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 2
          ->  Index Scan using tprt3_idx on tprt_3 (actual rows=0.33 loops=3)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 3
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 > tprt.col1
@@ -3141,17 +3190,23 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
    ->  Append (actual rows=1.00 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 > tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (actual rows=1.00 loops=1)
                Index Cond: (col1 > tbl1.col1)
-(15 rows)
+               Index Searches: 1
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 < tprt.col1
@@ -3173,17 +3228,23 @@ select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
    ->  Append (actual rows=0.00 loops=1)
          ->  Index Scan using tprt1_idx on tprt_1 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt2_idx on tprt_2 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt3_idx on tprt_3 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt4_idx on tprt_4 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt5_idx on tprt_5 (never executed)
                Index Cond: (col1 = tbl1.col1)
+               Index Searches: 0
          ->  Index Scan using tprt6_idx on tprt_6 (never executed)
                Index Cond: (col1 = tbl1.col1)
-(15 rows)
+               Index Searches: 0
+(21 rows)
 
 select tbl1.col1, tprt.col1 from tbl1
 inner join tprt on tbl1.col1 = tprt.col1
@@ -3513,10 +3574,12 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1.00 loops=1)
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
+         Index Searches: 1
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1.00 loops=1)
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(9 rows)
+         Index Searches: 1
+(11 rows)
 
 execute mt_q1(15);
  a  
@@ -3534,7 +3597,8 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1.00 loops=1)
          Filter: ((a >= $1) AND ((a % 10) = 5))
          Rows Removed by Filter: 9
-(6 rows)
+         Index Searches: 1
+(7 rows)
 
 execute mt_q1(25);
  a  
@@ -3582,13 +3646,17 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
              ->  Limit (actual rows=1.00 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1.00 loops=1)
                          Index Cond: (b IS NOT NULL)
+                         Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
          Filter: (a >= (InitPlan 2).col1)
+         Index Searches: 0
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10.00 loops=1)
          Filter: (a >= (InitPlan 2).col1)
+         Index Searches: 1
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10.00 loops=1)
          Filter: (a >= (InitPlan 2).col1)
-(14 rows)
+         Index Searches: 1
+(18 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -4159,13 +4227,17 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0.00 loops=1)
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
+               Index Searches: 1
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0.00 loops=1)
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
+               Index Searches: 1
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
                Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
+               Index Searches: 0
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0.00 loops=1)
          Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-(15 rows)
+         Index Searches: 1
+(19 rows)
 
 reset enable_sort;
 drop table rangep;
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index cd79abc35..bab0cc93f 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -764,7 +764,8 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
  Index Scan using onek2_u2_prtl on onek2 (actual rows=1.00 loops=1)
    Index Cond: (unique2 = 11)
    Filter: (stringu1 = 'ATAAAA'::name)
-(3 rows)
+   Index Searches: 1
+(4 rows)
 
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index d5aab4e56..c0d47fa87 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -23,8 +23,9 @@ begin
         ln := regexp_replace(ln, 'Evictions: 0', 'Evictions: Zero');
         ln := regexp_replace(ln, 'Evictions: \d+', 'Evictions: N');
         ln := regexp_replace(ln, 'Memory Usage: \d+', 'Memory Usage: N');
-	ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
-	ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N');
+        ln := regexp_replace(ln, 'loops=\d+', 'loops=N');
+        ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N');
         return next ln;
     end loop;
 end;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 5f36d589b..4a2c74b08 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -588,6 +588,10 @@ begin
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
         ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        perform regexp_matches(ln, 'Index Searches: \d+');
+        if found then
+          continue;
+        end if;
         return next ln;
     end loop;
 end;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 984006099..d60ae7c72 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1238,6 +1238,7 @@ IndexPath
 IndexRuntimeKeyInfo
 IndexScan
 IndexScanDesc
+IndexScanInstrumentation
 IndexScanState
 IndexStateFlagsAction
 IndexStmt
@@ -2666,6 +2667,7 @@ SharedExecutorInstrumentation
 SharedFileSet
 SharedHashInfo
 SharedIncrementalSortInfo
+SharedIndexScanInstrumentation;
 SharedInvalCatalogMsg
 SharedInvalCatcacheMsg
 SharedInvalRelcacheMsg
-- 
2.47.2

v27-0003-Improve-nbtree-SAOP-primitive-scan-scheduling.patchapplication/x-patch; name=v27-0003-Improve-nbtree-SAOP-primitive-scan-scheduling.patchDownload
From d0288a5b81fac42a5157b956fe15c28bb6335a25 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Fri, 14 Feb 2025 16:11:59 -0500
Subject: [PATCH v27 3/7] Improve nbtree SAOP primitive scan scheduling.

Add new primitive index scan scheduling heuristics that make
_bt_advance_array_keys avoid ending the ongoing primitive index scan
when it has already read more than one leaf page during the ongoing
primscan.  This tends to result in better decisions about how to start
and end primitive index scans with queries that have SAOP arrays with
many elements that are clustered together (e.g., contiguous integers).

Preparation for an upcoming patch that will add skip scan optimizations
to nbtree.  That'll work by adding skip arrays, which behave similarly
to SAOP arrays with many elements clustered together.

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wzkz0wPe6+02kr+hC+JJNKfGtjGTzpG3CFVTQmKwWNrXNw@mail.gmail.com
---
 src/include/access/nbtree.h           |   9 +-
 src/backend/access/nbtree/nbtsearch.c |  70 ++++----
 src/backend/access/nbtree/nbtutils.c  | 227 ++++++++++++++------------
 3 files changed, 169 insertions(+), 137 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 0c43767f8..c9bc82eba 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1043,8 +1043,8 @@ typedef struct BTScanOpaqueData
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
-	bool		scanBehind;		/* Last array advancement matched -inf attr? */
-	bool		oppositeDirCheck;	/* explicit scanBehind recheck needed? */
+	bool		scanBehind;		/* arrays might be ahead of scan's key space? */
+	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
 	BTArrayKeyInfo *arrayKeys;	/* info about each equality-type array key */
 	FmgrInfo   *orderProcs;		/* ORDER procs for required equality keys */
 	MemoryContext arrayContext; /* scan-lifespan context for array data */
@@ -1099,6 +1099,7 @@ typedef struct BTReadPageState
 	 * Input and output parameters, set and unset by both _bt_readpage and
 	 * _bt_checkkeys to manage precheck optimizations
 	 */
+	bool		firstpage;		/* on first page of current primitive scan? */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
 
@@ -1298,8 +1299,8 @@ extern int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir);
 extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 						  IndexTuple tuple, int tupnatts);
-extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
-								  IndexTuple finaltup);
+extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
+									 IndexTuple finaltup);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 22b27d01d..3fb0a0380 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -33,7 +33,7 @@ static OffsetNumber _bt_binsrch(Relation rel, BTScanInsert key, Buffer buf);
 static int	_bt_binsrch_posting(BTScanInsert key, Page page,
 								OffsetNumber offnum);
 static bool _bt_readpage(IndexScanDesc scan, ScanDirection dir,
-						 OffsetNumber offnum, bool firstPage);
+						 OffsetNumber offnum, bool firstpage);
 static void _bt_saveitem(BTScanOpaque so, int itemIndex,
 						 OffsetNumber offnum, IndexTuple itup);
 static int	_bt_setuppostingitems(BTScanOpaque so, int itemIndex,
@@ -1500,7 +1500,7 @@ _bt_next(IndexScanDesc scan, ScanDirection dir)
  */
 static bool
 _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
-			 bool firstPage)
+			 bool firstpage)
 {
 	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -1559,6 +1559,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.offnum = InvalidOffsetNumber;
 	pstate.skip = InvalidOffsetNumber;
 	pstate.continuescan = true; /* default assumption */
+	pstate.firstpage = firstpage;
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
 	pstate.rechecks = 0;
@@ -1604,7 +1605,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!firstPage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
@@ -1621,36 +1622,29 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	if (ScanDirectionIsForward(dir))
 	{
 		/* SK_SEARCHARRAY forward scans must provide high key up front */
-		if (arrayKeys && !P_RIGHTMOST(opaque))
+		if (arrayKeys)
 		{
-			ItemId		iid = PageGetItemId(page, P_HIKEY);
-
-			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
-
-			if (unlikely(so->oppositeDirCheck))
+			if (!P_RIGHTMOST(opaque))
 			{
-				Assert(so->scanBehind);
+				ItemId		iid = PageGetItemId(page, P_HIKEY);
 
-				/*
-				 * Last _bt_readpage call scheduled a recheck of finaltup for
-				 * required scan keys up to and including a > or >= scan key.
-				 *
-				 * _bt_checkkeys won't consider the scanBehind flag unless the
-				 * scan is stopped by a scan key required in the current scan
-				 * direction.  We need this recheck so that we'll notice when
-				 * all tuples on this page are still before the _bt_first-wise
-				 * start of matches for the current set of array keys.
-				 */
-				if (!_bt_oppodir_checkkeys(scan, dir, pstate.finaltup))
+				pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+				/* If a recheck is scheduled, check finaltup first */
+				if (unlikely(so->scanBehind) &&
+					!_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup))
 				{
 					/* Schedule another primitive index scan after all */
 					so->currPos.moreRight = false;
 					so->needPrimScan = true;
+					if (scan->parallel_scan)
+						_bt_parallel_primscan_schedule(scan,
+													   so->currPos.currPage);
 					return false;
 				}
-
-				/* Deliberately don't unset scanBehind flag just yet */
 			}
+
+			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
 		/* load items[] in ascending order */
@@ -1746,7 +1740,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 		 * only appear on non-pivot tuples on the right sibling page are
 		 * common.
 		 */
-		if (pstate.continuescan && !P_RIGHTMOST(opaque))
+		if (pstate.continuescan && !so->scanBehind && !P_RIGHTMOST(opaque))
 		{
 			ItemId		iid = PageGetItemId(page, P_HIKEY);
 			IndexTuple	itup = (IndexTuple) PageGetItem(page, iid);
@@ -1768,11 +1762,29 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	else
 	{
 		/* SK_SEARCHARRAY backward scans must provide final tuple up front */
-		if (arrayKeys && minoff <= maxoff && !P_LEFTMOST(opaque))
+		if (arrayKeys)
 		{
-			ItemId		iid = PageGetItemId(page, minoff);
+			if (minoff <= maxoff && !P_LEFTMOST(opaque))
+			{
+				ItemId		iid = PageGetItemId(page, minoff);
 
-			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+				pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+				/* If a recheck is scheduled, check finaltup first */
+				if (unlikely(so->scanBehind) &&
+					!_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup))
+				{
+					/* Schedule another primitive index scan after all */
+					so->currPos.moreLeft = false;
+					so->needPrimScan = true;
+					if (scan->parallel_scan)
+						_bt_parallel_primscan_schedule(scan,
+													   so->currPos.currPage);
+					return false;
+				}
+			}
+
+			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
 		/* load items[] in descending order */
@@ -2276,14 +2288,14 @@ _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno,
 			if (ScanDirectionIsForward(dir))
 			{
 				/* note that this will clear moreRight if we can stop */
-				if (_bt_readpage(scan, dir, P_FIRSTDATAKEY(opaque), false))
+				if (_bt_readpage(scan, dir, P_FIRSTDATAKEY(opaque), seized))
 					break;
 				blkno = so->currPos.nextPage;
 			}
 			else
 			{
 				/* note that this will clear moreLeft if we can stop */
-				if (_bt_readpage(scan, dir, PageGetMaxOffsetNumber(page), false))
+				if (_bt_readpage(scan, dir, PageGetMaxOffsetNumber(page), seized))
 					break;
 				blkno = so->currPos.prevPage;
 			}
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index efe58beaa..4e455a66b 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -42,6 +42,8 @@ static bool _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 static bool _bt_verify_arrays_bt_first(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_verify_keys_with_arraykeys(IndexScanDesc scan);
 #endif
+static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
 							  bool advancenonrequired, bool prechecked, bool firstmatch,
@@ -870,15 +872,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	int			arrayidx = 0;
 	bool		beyond_end_advance = false,
 				has_required_opposite_direction_only = false,
-				oppodir_inequality_sktrig = false,
 				all_required_satisfied = true,
 				all_satisfied = true;
 
-	/*
-	 * Unset so->scanBehind (and so->oppositeDirCheck) in case they're still
-	 * set from back when we dealt with the previous page's high key/finaltup
-	 */
-	so->scanBehind = so->oppositeDirCheck = false;
+	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	if (sktrig_required)
 	{
@@ -990,18 +987,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			beyond_end_advance = true;
 			all_satisfied = all_required_satisfied = false;
 
-			/*
-			 * Set a flag that remembers that this was an inequality required
-			 * in the opposite scan direction only, that nevertheless
-			 * triggered the call here.
-			 *
-			 * This only happens when an inequality operator (which must be
-			 * strict) encounters a group of NULLs that indicate the end of
-			 * non-NULL values for tuples in the current scan direction.
-			 */
-			if (unlikely(required_opposite_direction_only))
-				oppodir_inequality_sktrig = true;
-
 			continue;
 		}
 
@@ -1306,10 +1291,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * Note: we don't just quit at this point when all required scan keys were
 	 * found to be satisfied because we need to consider edge-cases involving
 	 * scan keys required in the opposite direction only; those aren't tracked
-	 * by all_required_satisfied. (Actually, oppodir_inequality_sktrig trigger
-	 * scan keys are tracked by all_required_satisfied, since it's convenient
-	 * for _bt_check_compare to behave as if they are required in the current
-	 * scan direction to deal with NULLs.  We'll account for that separately.)
+	 * by all_required_satisfied.
 	 */
 	Assert(_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts,
 										false, 0, NULL) ==
@@ -1343,7 +1325,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	/*
 	 * When we encounter a truncated finaltup high key attribute, we're
 	 * optimistic about the chances of its corresponding required scan key
-	 * being satisfied when we go on to check it against tuples from this
+	 * being satisfied when we go on to recheck it against tuples from this
 	 * page's right sibling leaf page.  We consider truncated attributes to be
 	 * satisfied by required scan keys, which allows the primitive index scan
 	 * to continue to the next leaf page.  We must set so->scanBehind to true
@@ -1365,28 +1347,24 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 *
 	 * You can think of this as a speculative bet on what the scan is likely
 	 * to find on the next page.  It's not much of a gamble, though, since the
-	 * untruncated prefix of attributes must strictly satisfy the new qual
-	 * (though it's okay if any non-required scan keys fail to be satisfied).
+	 * untruncated prefix of attributes must strictly satisfy the new qual.
 	 */
-	if (so->scanBehind && has_required_opposite_direction_only)
+	if (so->scanBehind)
 	{
 		/*
-		 * However, we need to work harder whenever the scan involves a scan
-		 * key required in the opposite direction to the scan only, along with
-		 * a finaltup with at least one truncated attribute that's associated
-		 * with a scan key marked required (required in either direction).
+		 * Truncated high key -- _bt_scanbehind_checkkeys recheck scheduled.
 		 *
-		 * _bt_check_compare simply won't stop the scan for a scan key that's
-		 * marked required in the opposite scan direction only.  That leaves
-		 * us without an automatic way of reconsidering any opposite-direction
-		 * inequalities if it turns out that starting a new primitive index
-		 * scan will allow _bt_first to skip ahead by a great many leaf pages.
-		 *
-		 * We deal with this by explicitly scheduling a finaltup recheck on
-		 * the right sibling page.  _bt_readpage calls _bt_oppodir_checkkeys
-		 * for next page's finaltup (and we skip it for this page's finaltup).
+		 * Remember if recheck needs to call _bt_oppodir_checkkeys for next
+		 * page's finaltup (see below comments about "Handle inequalities
+		 * marked required in the opposite scan direction" for why).
 		 */
-		so->oppositeDirCheck = true;	/* recheck next page's high key */
+		so->oppositeDirCheck = has_required_opposite_direction_only;
+
+		/*
+		 * Make sure that any SAOP arrays that were not marked required by
+		 * preprocessing are reset to their first element for this direction
+		 */
+		_bt_rewind_nonrequired_arrays(scan, dir);
 	}
 
 	/*
@@ -1411,11 +1389,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * (primitive) scan.  If this happens at the start of a large group of
 	 * NULL values, then we shouldn't expect to be called again until after
 	 * the scan has already read indefinitely-many leaf pages full of tuples
-	 * with NULL suffix values.  We need a separate test for this case so that
-	 * we don't miss our only opportunity to skip over such a group of pages.
-	 * (_bt_first is expected to skip over the group of NULLs by applying a
-	 * similar "deduce NOT NULL" rule, where it finishes its insertion scan
-	 * key by consing up an explicit SK_SEARCHNOTNULL key.)
+	 * with NULL suffix values.  (_bt_first is expected to skip over the group
+	 * of NULLs by applying a similar "deduce NOT NULL" rule of its own, which
+	 * involves consing up an explicit SK_SEARCHNOTNULL key.)
 	 *
 	 * Apply a test against finaltup to detect and recover from the problem:
 	 * if even finaltup doesn't satisfy such an inequality, we just skip by
@@ -1423,20 +1399,18 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * that all of the tuples on the current page following caller's tuple are
 	 * also before the _bt_first-wise start of tuples for our new qual.  That
 	 * at least suggests many more skippable pages beyond the current page.
-	 * (when so->oppositeDirCheck was set, this'll happen on the next page.)
+	 * (when so->scanBehind and so->oppositeDirCheck are set, this'll happen
+	 * when we test the next page's finaltup/high key instead.)
 	 */
 	else if (has_required_opposite_direction_only && pstate->finaltup &&
-			 (all_required_satisfied || oppodir_inequality_sktrig) &&
 			 unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup)))
 	{
-		/*
-		 * Make sure that any non-required arrays are set to the first array
-		 * element for the current scan direction
-		 */
 		_bt_rewind_nonrequired_arrays(scan, dir);
 		goto new_prim_scan;
 	}
 
+continue_scan:
+
 	/*
 	 * Stick with the ongoing primitive index scan for now.
 	 *
@@ -1458,8 +1432,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	if (so->scanBehind)
 	{
 		/* Optimization: skip by setting "look ahead" mechanism's offnum */
-		Assert(ScanDirectionIsForward(dir));
-		pstate->skip = pstate->maxoff + 1;
+		if (ScanDirectionIsForward(dir))
+			pstate->skip = pstate->maxoff + 1;
+		else
+			pstate->skip = pstate->minoff - 1;
 	}
 
 	/* Caller's tuple doesn't match the new qual */
@@ -1469,6 +1445,41 @@ new_prim_scan:
 
 	Assert(pstate->finaltup);	/* not on rightmost/leftmost page */
 
+	/*
+	 * Looks like another primitive index scan is required.  But consider
+	 * backing out and continuing the primscan based on scan-level heuristics.
+	 *
+	 * Continue the ongoing primitive scan (but schedule a recheck for when
+	 * the scan arrives on the next sibling leaf page) when it has already
+	 * read at least one leaf page before the one we're reading now.  This is
+	 * important when reading subsets of an index with many distinct values in
+	 * respect of an attribute constrained by an array.  It encourages fewer,
+	 * larger primitive scans where that makes sense.
+	 *
+	 * Note: so->scanBehind is primarily used to indicate that the scan
+	 * encountered a finaltup that "satisfied" one or more required scan keys
+	 * on a truncated attribute value/-inf value.  We can safely reuse it to
+	 * force the scan to stay on the leaf level because the considerations are
+	 * exactly the same.
+	 *
+	 * Note: This heuristic isn't as aggressive as you might think.  We're
+	 * conservative about allowing a primitive scan to step from the first
+	 * leaf page it reads to the page's sibling page (we only allow it on
+	 * first pages whose finaltup strongly suggests that it'll work out).
+	 * Clearing this first page finaltup hurdle is a strong signal in itself.
+	 */
+	if (!pstate->firstpage)
+	{
+		/* Schedule a recheck once on the next (or previous) page */
+		so->scanBehind = true;
+		so->oppositeDirCheck = has_required_opposite_direction_only;
+
+		_bt_rewind_nonrequired_arrays(scan, dir);
+
+		/* Continue the current primitive scan after all */
+		goto continue_scan;
+	}
+
 	/*
 	 * End this primitive index scan, but schedule another.
 	 *
@@ -1499,7 +1510,7 @@ end_toplevel_scan:
 	 * first positions for what will then be the current scan direction.
 	 */
 	pstate->continuescan = false;	/* Tell _bt_readpage we're done... */
-	so->needPrimScan = false;	/* ...don't call _bt_first again, though */
+	so->needPrimScan = false;	/* ...and don't call _bt_first again */
 
 	/* Caller's tuple doesn't match any qual */
 	return false;
@@ -1634,6 +1645,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
+	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
 							arrayKeys, pstate->prechecked, pstate->firstmatch,
@@ -1688,62 +1700,36 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
+		/* Override _bt_check_compare, continue primitive scan */
+		pstate->continuescan = true;
+
 		/*
-		 * Tuple is still before the start of matches according to the scan's
-		 * required array keys (according to _all_ of its required equality
-		 * strategy keys, actually).
+		 * We will end up here repeatedly given a group of tuples > the
+		 * previous array keys and < the now-current keys (for a backwards
+		 * scan it's just the same, though the operators swap positions).
 		 *
-		 * _bt_advance_array_keys occasionally sets so->scanBehind to signal
-		 * that the scan's current position/tuples might be significantly
-		 * behind (multiple pages behind) its current array keys.  When this
-		 * happens, we need to be prepared to recover by starting a new
-		 * primitive index scan here, on our own.
+		 * We must avoid allowing this linear search process to scan very many
+		 * tuples from well before the start of tuples matching the current
+		 * array keys (or from well before the point where we'll once again
+		 * have to advance the scan's array keys).
+		 *
+		 * We keep the overhead under control by speculatively "looking ahead"
+		 * to later still-unscanned items from this same leaf page.  We'll
+		 * only attempt this once the number of tuples that the linear search
+		 * process has examined starts to get out of hand.
 		 */
-		Assert(!so->scanBehind ||
-			   so->keyData[ikey].sk_strategy == BTEqualStrategyNumber);
-		if (unlikely(so->scanBehind) && pstate->finaltup &&
-			_bt_tuple_before_array_skeys(scan, dir, pstate->finaltup, tupdesc,
-										 BTreeTupleGetNAtts(pstate->finaltup,
-															scan->indexRelation),
-										 false, 0, NULL))
+		pstate->rechecks++;
+		if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
 		{
-			/* Cut our losses -- start a new primitive index scan now */
-			pstate->continuescan = false;
-			so->needPrimScan = true;
-		}
-		else
-		{
-			/* Override _bt_check_compare, continue primitive scan */
-			pstate->continuescan = true;
+			/* See if we should skip ahead within the current leaf page */
+			_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
 
 			/*
-			 * We will end up here repeatedly given a group of tuples > the
-			 * previous array keys and < the now-current keys (for a backwards
-			 * scan it's just the same, though the operators swap positions).
-			 *
-			 * We must avoid allowing this linear search process to scan very
-			 * many tuples from well before the start of tuples matching the
-			 * current array keys (or from well before the point where we'll
-			 * once again have to advance the scan's array keys).
-			 *
-			 * We keep the overhead under control by speculatively "looking
-			 * ahead" to later still-unscanned items from this same leaf page.
-			 * We'll only attempt this once the number of tuples that the
-			 * linear search process has examined starts to get out of hand.
+			 * Might have set pstate.skip to a later page offset.  When that
+			 * happens then _bt_readpage caller will inexpensively skip ahead
+			 * to a later tuple from the same page (the one just after the
+			 * tuple we successfully "looked ahead" to).
 			 */
-			pstate->rechecks++;
-			if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
-			{
-				/* See if we should skip ahead within the current leaf page */
-				_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
-
-				/*
-				 * Might have set pstate.skip to a later page offset.  When
-				 * that happens then _bt_readpage caller will inexpensively
-				 * skip ahead to a later tuple from the same page (the one
-				 * just after the tuple we successfully "looked ahead" to).
-				 */
-			}
 		}
 
 		/* This indextuple doesn't match the current qual, in any case */
@@ -1760,6 +1746,39 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 								  ikey, true);
 }
 
+/*
+ * Test whether finaltup (the final tuple on the page) is still before the
+ * start of matches for the current array keys.
+ *
+ * Caller's finaltup tuple is the page high key (for forwards scans), or the
+ * first non-pivot tuple (for backwards scans).  Called during scans with
+ * array keys when the so->scanBehind flag was set on the previous page.
+ *
+ * Returns false if the tuple is still before the start of matches.  When that
+ * happens caller should cut its losses and start a new primitive index scan.
+ * Otherwise returns true.
+ */
+bool
+_bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
+						 IndexTuple finaltup)
+{
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	int			nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+
+	Assert(so->numArrayKeys);
+
+	if (_bt_tuple_before_array_skeys(scan, dir, finaltup, tupdesc,
+									 nfinaltupatts, false, 0, NULL))
+		return false;
+
+	if (!so->oppositeDirCheck)
+		return true;
+
+	return _bt_oppodir_checkkeys(scan, dir, finaltup);
+}
+
 /*
  * Test whether an indextuple fails to satisfy an inequality required in the
  * opposite direction only.
@@ -1778,7 +1797,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
  * _bt_checkkeys to stop the scan to consider array advancement/starting a new
  * primitive index scan.
  */
-bool
+static bool
 _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 					  IndexTuple finaltup)
 {
-- 
2.47.2

v27-0002-Make-BTMaxItemSize-macro-not-require-a-Page-arg.patchapplication/x-patch; name=v27-0002-Make-BTMaxItemSize-macro-not-require-a-Page-arg.patchDownload
From 5b7cee86b7b996a6dbc5b5c3901960963f037a0d Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 3 Mar 2025 10:56:18 -0500
Subject: [PATCH v27 2/7] Make BTMaxItemSize macro not require a Page arg.

---
 src/include/access/nbtree.h           | 8 ++++----
 src/backend/access/nbtree/nbtdedup.c  | 6 +++---
 src/backend/access/nbtree/nbtinsert.c | 2 +-
 src/backend/access/nbtree/nbtsort.c   | 4 ++--
 src/backend/access/nbtree/nbtutils.c  | 7 +++----
 src/backend/access/nbtree/nbtxlog.c   | 2 +-
 contrib/amcheck/verify_nbtree.c       | 3 +--
 7 files changed, 15 insertions(+), 17 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index e4fdeca34..0c43767f8 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -161,13 +161,13 @@ typedef struct BTMetaPageData
  * a heap index tuple to make space for a tiebreaker heap TID
  * attribute, which we account for here.
  */
-#define BTMaxItemSize(page) \
-	(MAXALIGN_DOWN((PageGetPageSize(page) - \
+#define BTMaxItemSize \
+	(MAXALIGN_DOWN((BLCKSZ - \
 					MAXALIGN(SizeOfPageHeaderData + 3*sizeof(ItemIdData)) - \
 					MAXALIGN(sizeof(BTPageOpaqueData))) / 3) - \
 					MAXALIGN(sizeof(ItemPointerData)))
-#define BTMaxItemSizeNoHeapTid(page) \
-	MAXALIGN_DOWN((PageGetPageSize(page) - \
+#define BTMaxItemSizeNoHeapTid \
+	MAXALIGN_DOWN((BLCKSZ - \
 				   MAXALIGN(SizeOfPageHeaderData + 3*sizeof(ItemIdData)) - \
 				   MAXALIGN(sizeof(BTPageOpaqueData))) / 3)
 
diff --git a/src/backend/access/nbtree/nbtdedup.c b/src/backend/access/nbtree/nbtdedup.c
index cbe73675f..08884116a 100644
--- a/src/backend/access/nbtree/nbtdedup.c
+++ b/src/backend/access/nbtree/nbtdedup.c
@@ -84,7 +84,7 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz,
 	state = (BTDedupState) palloc(sizeof(BTDedupStateData));
 	state->deduplicate = true;
 	state->nmaxitems = 0;
-	state->maxpostingsize = Min(BTMaxItemSize(page) / 2, INDEX_SIZE_MASK);
+	state->maxpostingsize = Min(BTMaxItemSize / 2, INDEX_SIZE_MASK);
 	/* Metadata about base tuple of current pending posting list */
 	state->base = NULL;
 	state->baseoff = InvalidOffsetNumber;
@@ -568,7 +568,7 @@ _bt_dedup_finish_pending(Page newpage, BTDedupState state)
 		/* Use original, unchanged base tuple */
 		tuplesz = IndexTupleSize(state->base);
 		Assert(tuplesz == MAXALIGN(IndexTupleSize(state->base)));
-		Assert(tuplesz <= BTMaxItemSize(newpage));
+		Assert(tuplesz <= BTMaxItemSize);
 		if (PageAddItem(newpage, (Item) state->base, tuplesz, tupoff,
 						false, false) == InvalidOffsetNumber)
 			elog(ERROR, "deduplication failed to add tuple to page");
@@ -588,7 +588,7 @@ _bt_dedup_finish_pending(Page newpage, BTDedupState state)
 		state->intervals[state->nintervals].nitems = state->nitems;
 
 		Assert(tuplesz == MAXALIGN(IndexTupleSize(final)));
-		Assert(tuplesz <= BTMaxItemSize(newpage));
+		Assert(tuplesz <= BTMaxItemSize);
 		if (PageAddItem(newpage, (Item) final, tuplesz, tupoff, false,
 						false) == InvalidOffsetNumber)
 			elog(ERROR, "deduplication failed to add tuple to page");
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index 31fe1c3ad..aa82cede3 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -827,7 +827,7 @@ _bt_findinsertloc(Relation rel,
 	opaque = BTPageGetOpaque(page);
 
 	/* Check 1/3 of a page restriction */
-	if (unlikely(insertstate->itemsz > BTMaxItemSize(page)))
+	if (unlikely(insertstate->itemsz > BTMaxItemSize))
 		_bt_check_third_page(rel, heapRel, itup_key->heapkeyspace, page,
 							 insertstate->itup);
 
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index 7aba852db..fa336ba00 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -829,7 +829,7 @@ _bt_buildadd(BTWriteState *wstate, BTPageState *state, IndexTuple itup,
 	 * make use of the reserved space.  This should never fail on internal
 	 * pages.
 	 */
-	if (unlikely(itupsz > BTMaxItemSize(npage)))
+	if (unlikely(itupsz > BTMaxItemSize))
 		_bt_check_third_page(wstate->index, wstate->heap, isleaf, npage,
 							 itup);
 
@@ -1305,7 +1305,7 @@ _bt_load(BTWriteState *wstate, BTSpool *btspool, BTSpool *btspool2)
 				 */
 				dstate->maxpostingsize = MAXALIGN_DOWN((BLCKSZ * 10 / 100)) -
 					sizeof(ItemIdData);
-				Assert(dstate->maxpostingsize <= BTMaxItemSize((Page) state->btps_buf) &&
+				Assert(dstate->maxpostingsize <= BTMaxItemSize &&
 					   dstate->maxpostingsize <= INDEX_SIZE_MASK);
 				dstate->htids = palloc(dstate->maxpostingsize);
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 693e43c67..efe58beaa 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -3245,7 +3245,7 @@ _bt_check_third_page(Relation rel, Relation heap, bool needheaptidspace,
 	itemsz = MAXALIGN(IndexTupleSize(newtup));
 
 	/* Double check item size against limit */
-	if (itemsz <= BTMaxItemSize(page))
+	if (itemsz <= BTMaxItemSize)
 		return;
 
 	/*
@@ -3253,7 +3253,7 @@ _bt_check_third_page(Relation rel, Relation heap, bool needheaptidspace,
 	 * index uses version 2 or version 3, or that page is an internal page, in
 	 * which case a slightly higher limit applies.
 	 */
-	if (!needheaptidspace && itemsz <= BTMaxItemSizeNoHeapTid(page))
+	if (!needheaptidspace && itemsz <= BTMaxItemSizeNoHeapTid)
 		return;
 
 	/*
@@ -3270,8 +3270,7 @@ _bt_check_third_page(Relation rel, Relation heap, bool needheaptidspace,
 			 errmsg("index row size %zu exceeds btree version %u maximum %zu for index \"%s\"",
 					itemsz,
 					needheaptidspace ? BTREE_VERSION : BTREE_NOVAC_VERSION,
-					needheaptidspace ? BTMaxItemSize(page) :
-					BTMaxItemSizeNoHeapTid(page),
+					needheaptidspace ? BTMaxItemSize : BTMaxItemSizeNoHeapTid,
 					RelationGetRelationName(rel)),
 			 errdetail("Index row references tuple (%u,%u) in relation \"%s\".",
 					   ItemPointerGetBlockNumber(BTreeTupleGetHeapTID(newtup)),
diff --git a/src/backend/access/nbtree/nbtxlog.c b/src/backend/access/nbtree/nbtxlog.c
index fadd06179..d31dd5673 100644
--- a/src/backend/access/nbtree/nbtxlog.c
+++ b/src/backend/access/nbtree/nbtxlog.c
@@ -483,7 +483,7 @@ btree_xlog_dedup(XLogReaderState *record)
 		state->deduplicate = true;	/* unused */
 		state->nmaxitems = 0;	/* unused */
 		/* Conservatively use larger maxpostingsize than primary */
-		state->maxpostingsize = BTMaxItemSize(page);
+		state->maxpostingsize = BTMaxItemSize;
 		state->base = NULL;
 		state->baseoff = InvalidOffsetNumber;
 		state->basetupsize = 0;
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index aac8c74f5..825b677c4 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -1597,8 +1597,7 @@ bt_target_page_check(BtreeCheckState *state)
 		 */
 		lowersizelimit = skey->heapkeyspace &&
 			(P_ISLEAF(topaque) || BTreeTupleGetHeapTID(itup) == NULL);
-		if (tupsize > (lowersizelimit ? BTMaxItemSize(state->target) :
-					   BTMaxItemSizeNoHeapTid(state->target)))
+		if (tupsize > (lowersizelimit ? BTMaxItemSize : BTMaxItemSizeNoHeapTid))
 		{
 			ItemPointer tid = BTreeTupleGetPointsToTID(itup);
 			char	   *itid,
-- 
2.47.2

In reply to: Peter Geoghegan (#67)
5 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Sat, Mar 8, 2025 at 11:43 AM Peter Geoghegan <pg@bowt.ie> wrote:

I plan on committing this one soon. It's obviously pretty pointless to
make the BTMaxItemSize operate off of a page header, and not requiring
it is more flexible.

Committed. And committed a revised version of "Show index search count
in EXPLAIN ANALYZE" that addresses the issues with non-parallel-aware
index scan executor nodes that run from a parallel worker.

Attached is v28. This is just to keep the patch series applying
cleanly -- no real changes here.

--
Peter Geoghegan

Attachments:

v28-0001-Improve-nbtree-SAOP-primitive-scan-scheduling.patchapplication/octet-stream; name=v28-0001-Improve-nbtree-SAOP-primitive-scan-scheduling.patchDownload
From a1330f130be6b979f19c475d841c579c5bb17e4a Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Fri, 14 Feb 2025 16:11:59 -0500
Subject: [PATCH v28 1/5] Improve nbtree SAOP primitive scan scheduling.

Add new primitive index scan scheduling heuristics that make
_bt_advance_array_keys avoid ending the ongoing primitive index scan
when it has already read more than one leaf page during the ongoing
primscan.  This tends to result in better decisions about how to start
and end primitive index scans with queries that have SAOP arrays with
many elements that are clustered together (e.g., contiguous integers).

Preparation for an upcoming patch that will add skip scan optimizations
to nbtree.  That'll work by adding skip arrays, which behave similarly
to SAOP arrays with many elements clustered together.

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wzkz0wPe6+02kr+hC+JJNKfGtjGTzpG3CFVTQmKwWNrXNw@mail.gmail.com
---
 src/include/access/nbtree.h           |   9 +-
 src/backend/access/nbtree/nbtsearch.c |  70 ++++----
 src/backend/access/nbtree/nbtutils.c  | 227 ++++++++++++++------------
 3 files changed, 169 insertions(+), 137 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 0c43767f8..c9bc82eba 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1043,8 +1043,8 @@ typedef struct BTScanOpaqueData
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
-	bool		scanBehind;		/* Last array advancement matched -inf attr? */
-	bool		oppositeDirCheck;	/* explicit scanBehind recheck needed? */
+	bool		scanBehind;		/* arrays might be ahead of scan's key space? */
+	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
 	BTArrayKeyInfo *arrayKeys;	/* info about each equality-type array key */
 	FmgrInfo   *orderProcs;		/* ORDER procs for required equality keys */
 	MemoryContext arrayContext; /* scan-lifespan context for array data */
@@ -1099,6 +1099,7 @@ typedef struct BTReadPageState
 	 * Input and output parameters, set and unset by both _bt_readpage and
 	 * _bt_checkkeys to manage precheck optimizations
 	 */
+	bool		firstpage;		/* on first page of current primitive scan? */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
 
@@ -1298,8 +1299,8 @@ extern int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir);
 extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 						  IndexTuple tuple, int tupnatts);
-extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
-								  IndexTuple finaltup);
+extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
+									 IndexTuple finaltup);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 22b27d01d..3fb0a0380 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -33,7 +33,7 @@ static OffsetNumber _bt_binsrch(Relation rel, BTScanInsert key, Buffer buf);
 static int	_bt_binsrch_posting(BTScanInsert key, Page page,
 								OffsetNumber offnum);
 static bool _bt_readpage(IndexScanDesc scan, ScanDirection dir,
-						 OffsetNumber offnum, bool firstPage);
+						 OffsetNumber offnum, bool firstpage);
 static void _bt_saveitem(BTScanOpaque so, int itemIndex,
 						 OffsetNumber offnum, IndexTuple itup);
 static int	_bt_setuppostingitems(BTScanOpaque so, int itemIndex,
@@ -1500,7 +1500,7 @@ _bt_next(IndexScanDesc scan, ScanDirection dir)
  */
 static bool
 _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
-			 bool firstPage)
+			 bool firstpage)
 {
 	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -1559,6 +1559,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.offnum = InvalidOffsetNumber;
 	pstate.skip = InvalidOffsetNumber;
 	pstate.continuescan = true; /* default assumption */
+	pstate.firstpage = firstpage;
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
 	pstate.rechecks = 0;
@@ -1604,7 +1605,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!firstPage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
@@ -1621,36 +1622,29 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	if (ScanDirectionIsForward(dir))
 	{
 		/* SK_SEARCHARRAY forward scans must provide high key up front */
-		if (arrayKeys && !P_RIGHTMOST(opaque))
+		if (arrayKeys)
 		{
-			ItemId		iid = PageGetItemId(page, P_HIKEY);
-
-			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
-
-			if (unlikely(so->oppositeDirCheck))
+			if (!P_RIGHTMOST(opaque))
 			{
-				Assert(so->scanBehind);
+				ItemId		iid = PageGetItemId(page, P_HIKEY);
 
-				/*
-				 * Last _bt_readpage call scheduled a recheck of finaltup for
-				 * required scan keys up to and including a > or >= scan key.
-				 *
-				 * _bt_checkkeys won't consider the scanBehind flag unless the
-				 * scan is stopped by a scan key required in the current scan
-				 * direction.  We need this recheck so that we'll notice when
-				 * all tuples on this page are still before the _bt_first-wise
-				 * start of matches for the current set of array keys.
-				 */
-				if (!_bt_oppodir_checkkeys(scan, dir, pstate.finaltup))
+				pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+				/* If a recheck is scheduled, check finaltup first */
+				if (unlikely(so->scanBehind) &&
+					!_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup))
 				{
 					/* Schedule another primitive index scan after all */
 					so->currPos.moreRight = false;
 					so->needPrimScan = true;
+					if (scan->parallel_scan)
+						_bt_parallel_primscan_schedule(scan,
+													   so->currPos.currPage);
 					return false;
 				}
-
-				/* Deliberately don't unset scanBehind flag just yet */
 			}
+
+			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
 		/* load items[] in ascending order */
@@ -1746,7 +1740,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 		 * only appear on non-pivot tuples on the right sibling page are
 		 * common.
 		 */
-		if (pstate.continuescan && !P_RIGHTMOST(opaque))
+		if (pstate.continuescan && !so->scanBehind && !P_RIGHTMOST(opaque))
 		{
 			ItemId		iid = PageGetItemId(page, P_HIKEY);
 			IndexTuple	itup = (IndexTuple) PageGetItem(page, iid);
@@ -1768,11 +1762,29 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	else
 	{
 		/* SK_SEARCHARRAY backward scans must provide final tuple up front */
-		if (arrayKeys && minoff <= maxoff && !P_LEFTMOST(opaque))
+		if (arrayKeys)
 		{
-			ItemId		iid = PageGetItemId(page, minoff);
+			if (minoff <= maxoff && !P_LEFTMOST(opaque))
+			{
+				ItemId		iid = PageGetItemId(page, minoff);
 
-			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+				pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+				/* If a recheck is scheduled, check finaltup first */
+				if (unlikely(so->scanBehind) &&
+					!_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup))
+				{
+					/* Schedule another primitive index scan after all */
+					so->currPos.moreLeft = false;
+					so->needPrimScan = true;
+					if (scan->parallel_scan)
+						_bt_parallel_primscan_schedule(scan,
+													   so->currPos.currPage);
+					return false;
+				}
+			}
+
+			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
 		/* load items[] in descending order */
@@ -2276,14 +2288,14 @@ _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno,
 			if (ScanDirectionIsForward(dir))
 			{
 				/* note that this will clear moreRight if we can stop */
-				if (_bt_readpage(scan, dir, P_FIRSTDATAKEY(opaque), false))
+				if (_bt_readpage(scan, dir, P_FIRSTDATAKEY(opaque), seized))
 					break;
 				blkno = so->currPos.nextPage;
 			}
 			else
 			{
 				/* note that this will clear moreLeft if we can stop */
-				if (_bt_readpage(scan, dir, PageGetMaxOffsetNumber(page), false))
+				if (_bt_readpage(scan, dir, PageGetMaxOffsetNumber(page), seized))
 					break;
 				blkno = so->currPos.prevPage;
 			}
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index efe58beaa..4e455a66b 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -42,6 +42,8 @@ static bool _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 static bool _bt_verify_arrays_bt_first(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_verify_keys_with_arraykeys(IndexScanDesc scan);
 #endif
+static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
 							  bool advancenonrequired, bool prechecked, bool firstmatch,
@@ -870,15 +872,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	int			arrayidx = 0;
 	bool		beyond_end_advance = false,
 				has_required_opposite_direction_only = false,
-				oppodir_inequality_sktrig = false,
 				all_required_satisfied = true,
 				all_satisfied = true;
 
-	/*
-	 * Unset so->scanBehind (and so->oppositeDirCheck) in case they're still
-	 * set from back when we dealt with the previous page's high key/finaltup
-	 */
-	so->scanBehind = so->oppositeDirCheck = false;
+	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	if (sktrig_required)
 	{
@@ -990,18 +987,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			beyond_end_advance = true;
 			all_satisfied = all_required_satisfied = false;
 
-			/*
-			 * Set a flag that remembers that this was an inequality required
-			 * in the opposite scan direction only, that nevertheless
-			 * triggered the call here.
-			 *
-			 * This only happens when an inequality operator (which must be
-			 * strict) encounters a group of NULLs that indicate the end of
-			 * non-NULL values for tuples in the current scan direction.
-			 */
-			if (unlikely(required_opposite_direction_only))
-				oppodir_inequality_sktrig = true;
-
 			continue;
 		}
 
@@ -1306,10 +1291,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * Note: we don't just quit at this point when all required scan keys were
 	 * found to be satisfied because we need to consider edge-cases involving
 	 * scan keys required in the opposite direction only; those aren't tracked
-	 * by all_required_satisfied. (Actually, oppodir_inequality_sktrig trigger
-	 * scan keys are tracked by all_required_satisfied, since it's convenient
-	 * for _bt_check_compare to behave as if they are required in the current
-	 * scan direction to deal with NULLs.  We'll account for that separately.)
+	 * by all_required_satisfied.
 	 */
 	Assert(_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts,
 										false, 0, NULL) ==
@@ -1343,7 +1325,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	/*
 	 * When we encounter a truncated finaltup high key attribute, we're
 	 * optimistic about the chances of its corresponding required scan key
-	 * being satisfied when we go on to check it against tuples from this
+	 * being satisfied when we go on to recheck it against tuples from this
 	 * page's right sibling leaf page.  We consider truncated attributes to be
 	 * satisfied by required scan keys, which allows the primitive index scan
 	 * to continue to the next leaf page.  We must set so->scanBehind to true
@@ -1365,28 +1347,24 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 *
 	 * You can think of this as a speculative bet on what the scan is likely
 	 * to find on the next page.  It's not much of a gamble, though, since the
-	 * untruncated prefix of attributes must strictly satisfy the new qual
-	 * (though it's okay if any non-required scan keys fail to be satisfied).
+	 * untruncated prefix of attributes must strictly satisfy the new qual.
 	 */
-	if (so->scanBehind && has_required_opposite_direction_only)
+	if (so->scanBehind)
 	{
 		/*
-		 * However, we need to work harder whenever the scan involves a scan
-		 * key required in the opposite direction to the scan only, along with
-		 * a finaltup with at least one truncated attribute that's associated
-		 * with a scan key marked required (required in either direction).
+		 * Truncated high key -- _bt_scanbehind_checkkeys recheck scheduled.
 		 *
-		 * _bt_check_compare simply won't stop the scan for a scan key that's
-		 * marked required in the opposite scan direction only.  That leaves
-		 * us without an automatic way of reconsidering any opposite-direction
-		 * inequalities if it turns out that starting a new primitive index
-		 * scan will allow _bt_first to skip ahead by a great many leaf pages.
-		 *
-		 * We deal with this by explicitly scheduling a finaltup recheck on
-		 * the right sibling page.  _bt_readpage calls _bt_oppodir_checkkeys
-		 * for next page's finaltup (and we skip it for this page's finaltup).
+		 * Remember if recheck needs to call _bt_oppodir_checkkeys for next
+		 * page's finaltup (see below comments about "Handle inequalities
+		 * marked required in the opposite scan direction" for why).
 		 */
-		so->oppositeDirCheck = true;	/* recheck next page's high key */
+		so->oppositeDirCheck = has_required_opposite_direction_only;
+
+		/*
+		 * Make sure that any SAOP arrays that were not marked required by
+		 * preprocessing are reset to their first element for this direction
+		 */
+		_bt_rewind_nonrequired_arrays(scan, dir);
 	}
 
 	/*
@@ -1411,11 +1389,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * (primitive) scan.  If this happens at the start of a large group of
 	 * NULL values, then we shouldn't expect to be called again until after
 	 * the scan has already read indefinitely-many leaf pages full of tuples
-	 * with NULL suffix values.  We need a separate test for this case so that
-	 * we don't miss our only opportunity to skip over such a group of pages.
-	 * (_bt_first is expected to skip over the group of NULLs by applying a
-	 * similar "deduce NOT NULL" rule, where it finishes its insertion scan
-	 * key by consing up an explicit SK_SEARCHNOTNULL key.)
+	 * with NULL suffix values.  (_bt_first is expected to skip over the group
+	 * of NULLs by applying a similar "deduce NOT NULL" rule of its own, which
+	 * involves consing up an explicit SK_SEARCHNOTNULL key.)
 	 *
 	 * Apply a test against finaltup to detect and recover from the problem:
 	 * if even finaltup doesn't satisfy such an inequality, we just skip by
@@ -1423,20 +1399,18 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * that all of the tuples on the current page following caller's tuple are
 	 * also before the _bt_first-wise start of tuples for our new qual.  That
 	 * at least suggests many more skippable pages beyond the current page.
-	 * (when so->oppositeDirCheck was set, this'll happen on the next page.)
+	 * (when so->scanBehind and so->oppositeDirCheck are set, this'll happen
+	 * when we test the next page's finaltup/high key instead.)
 	 */
 	else if (has_required_opposite_direction_only && pstate->finaltup &&
-			 (all_required_satisfied || oppodir_inequality_sktrig) &&
 			 unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup)))
 	{
-		/*
-		 * Make sure that any non-required arrays are set to the first array
-		 * element for the current scan direction
-		 */
 		_bt_rewind_nonrequired_arrays(scan, dir);
 		goto new_prim_scan;
 	}
 
+continue_scan:
+
 	/*
 	 * Stick with the ongoing primitive index scan for now.
 	 *
@@ -1458,8 +1432,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	if (so->scanBehind)
 	{
 		/* Optimization: skip by setting "look ahead" mechanism's offnum */
-		Assert(ScanDirectionIsForward(dir));
-		pstate->skip = pstate->maxoff + 1;
+		if (ScanDirectionIsForward(dir))
+			pstate->skip = pstate->maxoff + 1;
+		else
+			pstate->skip = pstate->minoff - 1;
 	}
 
 	/* Caller's tuple doesn't match the new qual */
@@ -1469,6 +1445,41 @@ new_prim_scan:
 
 	Assert(pstate->finaltup);	/* not on rightmost/leftmost page */
 
+	/*
+	 * Looks like another primitive index scan is required.  But consider
+	 * backing out and continuing the primscan based on scan-level heuristics.
+	 *
+	 * Continue the ongoing primitive scan (but schedule a recheck for when
+	 * the scan arrives on the next sibling leaf page) when it has already
+	 * read at least one leaf page before the one we're reading now.  This is
+	 * important when reading subsets of an index with many distinct values in
+	 * respect of an attribute constrained by an array.  It encourages fewer,
+	 * larger primitive scans where that makes sense.
+	 *
+	 * Note: so->scanBehind is primarily used to indicate that the scan
+	 * encountered a finaltup that "satisfied" one or more required scan keys
+	 * on a truncated attribute value/-inf value.  We can safely reuse it to
+	 * force the scan to stay on the leaf level because the considerations are
+	 * exactly the same.
+	 *
+	 * Note: This heuristic isn't as aggressive as you might think.  We're
+	 * conservative about allowing a primitive scan to step from the first
+	 * leaf page it reads to the page's sibling page (we only allow it on
+	 * first pages whose finaltup strongly suggests that it'll work out).
+	 * Clearing this first page finaltup hurdle is a strong signal in itself.
+	 */
+	if (!pstate->firstpage)
+	{
+		/* Schedule a recheck once on the next (or previous) page */
+		so->scanBehind = true;
+		so->oppositeDirCheck = has_required_opposite_direction_only;
+
+		_bt_rewind_nonrequired_arrays(scan, dir);
+
+		/* Continue the current primitive scan after all */
+		goto continue_scan;
+	}
+
 	/*
 	 * End this primitive index scan, but schedule another.
 	 *
@@ -1499,7 +1510,7 @@ end_toplevel_scan:
 	 * first positions for what will then be the current scan direction.
 	 */
 	pstate->continuescan = false;	/* Tell _bt_readpage we're done... */
-	so->needPrimScan = false;	/* ...don't call _bt_first again, though */
+	so->needPrimScan = false;	/* ...and don't call _bt_first again */
 
 	/* Caller's tuple doesn't match any qual */
 	return false;
@@ -1634,6 +1645,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
+	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
 							arrayKeys, pstate->prechecked, pstate->firstmatch,
@@ -1688,62 +1700,36 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
+		/* Override _bt_check_compare, continue primitive scan */
+		pstate->continuescan = true;
+
 		/*
-		 * Tuple is still before the start of matches according to the scan's
-		 * required array keys (according to _all_ of its required equality
-		 * strategy keys, actually).
+		 * We will end up here repeatedly given a group of tuples > the
+		 * previous array keys and < the now-current keys (for a backwards
+		 * scan it's just the same, though the operators swap positions).
 		 *
-		 * _bt_advance_array_keys occasionally sets so->scanBehind to signal
-		 * that the scan's current position/tuples might be significantly
-		 * behind (multiple pages behind) its current array keys.  When this
-		 * happens, we need to be prepared to recover by starting a new
-		 * primitive index scan here, on our own.
+		 * We must avoid allowing this linear search process to scan very many
+		 * tuples from well before the start of tuples matching the current
+		 * array keys (or from well before the point where we'll once again
+		 * have to advance the scan's array keys).
+		 *
+		 * We keep the overhead under control by speculatively "looking ahead"
+		 * to later still-unscanned items from this same leaf page.  We'll
+		 * only attempt this once the number of tuples that the linear search
+		 * process has examined starts to get out of hand.
 		 */
-		Assert(!so->scanBehind ||
-			   so->keyData[ikey].sk_strategy == BTEqualStrategyNumber);
-		if (unlikely(so->scanBehind) && pstate->finaltup &&
-			_bt_tuple_before_array_skeys(scan, dir, pstate->finaltup, tupdesc,
-										 BTreeTupleGetNAtts(pstate->finaltup,
-															scan->indexRelation),
-										 false, 0, NULL))
+		pstate->rechecks++;
+		if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
 		{
-			/* Cut our losses -- start a new primitive index scan now */
-			pstate->continuescan = false;
-			so->needPrimScan = true;
-		}
-		else
-		{
-			/* Override _bt_check_compare, continue primitive scan */
-			pstate->continuescan = true;
+			/* See if we should skip ahead within the current leaf page */
+			_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
 
 			/*
-			 * We will end up here repeatedly given a group of tuples > the
-			 * previous array keys and < the now-current keys (for a backwards
-			 * scan it's just the same, though the operators swap positions).
-			 *
-			 * We must avoid allowing this linear search process to scan very
-			 * many tuples from well before the start of tuples matching the
-			 * current array keys (or from well before the point where we'll
-			 * once again have to advance the scan's array keys).
-			 *
-			 * We keep the overhead under control by speculatively "looking
-			 * ahead" to later still-unscanned items from this same leaf page.
-			 * We'll only attempt this once the number of tuples that the
-			 * linear search process has examined starts to get out of hand.
+			 * Might have set pstate.skip to a later page offset.  When that
+			 * happens then _bt_readpage caller will inexpensively skip ahead
+			 * to a later tuple from the same page (the one just after the
+			 * tuple we successfully "looked ahead" to).
 			 */
-			pstate->rechecks++;
-			if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
-			{
-				/* See if we should skip ahead within the current leaf page */
-				_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
-
-				/*
-				 * Might have set pstate.skip to a later page offset.  When
-				 * that happens then _bt_readpage caller will inexpensively
-				 * skip ahead to a later tuple from the same page (the one
-				 * just after the tuple we successfully "looked ahead" to).
-				 */
-			}
 		}
 
 		/* This indextuple doesn't match the current qual, in any case */
@@ -1760,6 +1746,39 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 								  ikey, true);
 }
 
+/*
+ * Test whether finaltup (the final tuple on the page) is still before the
+ * start of matches for the current array keys.
+ *
+ * Caller's finaltup tuple is the page high key (for forwards scans), or the
+ * first non-pivot tuple (for backwards scans).  Called during scans with
+ * array keys when the so->scanBehind flag was set on the previous page.
+ *
+ * Returns false if the tuple is still before the start of matches.  When that
+ * happens caller should cut its losses and start a new primitive index scan.
+ * Otherwise returns true.
+ */
+bool
+_bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
+						 IndexTuple finaltup)
+{
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	int			nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+
+	Assert(so->numArrayKeys);
+
+	if (_bt_tuple_before_array_skeys(scan, dir, finaltup, tupdesc,
+									 nfinaltupatts, false, 0, NULL))
+		return false;
+
+	if (!so->oppositeDirCheck)
+		return true;
+
+	return _bt_oppodir_checkkeys(scan, dir, finaltup);
+}
+
 /*
  * Test whether an indextuple fails to satisfy an inequality required in the
  * opposite direction only.
@@ -1778,7 +1797,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
  * _bt_checkkeys to stop the scan to consider array advancement/starting a new
  * primitive index scan.
  */
-bool
+static bool
 _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 					  IndexTuple finaltup)
 {
-- 
2.47.2

v28-0004-Apply-low-order-skip-key-in-_bt_first-more-often.patchapplication/octet-stream; name=v28-0004-Apply-low-order-skip-key-in-_bt_first-more-often.patchDownload
From fcf4e29053c66eaacd7216e6802a5774031287a9 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 13 Jan 2025 16:08:32 -0500
Subject: [PATCH v28 4/5] Apply low-order skip key in _bt_first more often.

Convert low_compare and high_compare nbtree skip array inequalities
(with opclasses that offer skip support) such that _bt_first is
consistently able to use later keys when descending the tree within
_bt_first.

For example, an index qual "WHERE a > 5 AND b = 2" is now converted to
"WHERE a >= 6 AND b = 2" by a new preprocessing step that takes place
after a final low_compare and/or high_compare are chosen by all earlier
preprocessing steps.  That way the scan's initial call to _bt_first will
use "WHERE a >= 6 AND b = 2" to find the initial leaf level position,
rather than merely using "WHERE a > 5" -- "b = 2" can always be applied.
This has a decent chance of making the scan avoid an extra _bt_first
call that would otherwise be needed just to determine the lowest-sorting
"a" value in the index (the lowest that still satisfies "WHERE a > 5").

The transformation process can only lower the total number of index
pages read when the use of a more restrictive set of initial positioning
keys in _bt_first actually allows the scan to land on some later leaf
page directly, relative to the unoptimized case (or on an earlier leaf
page directly, when scanning backwards).  The savings can be far greater
when affected skip arrays come after some higher-order array.  For
example, a qual "WHERE x IN (1, 2, 3) AND y > 5 AND z = 2" can now save
as many as 3 _bt_first calls as a result of these transformations (there
can be as many as 1 _bt_first call saved per "x" array element).

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=FJ78K3WsF3iWNxWnUCY9f=Jdg3QPxaXE=uYUbmuRz5Q@mail.gmail.com
---
 src/backend/access/nbtree/nbtpreprocesskeys.c | 181 ++++++++++++++++++
 src/test/regress/expected/create_index.out    |  21 ++
 src/test/regress/sql/create_index.sql         |  10 +
 3 files changed, 212 insertions(+)

diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index bd993b478..c48c4f6c1 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -50,6 +50,12 @@ static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
 								 BTArrayKeyInfo *array, bool *qual_ok);
 static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
 								 BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+									   BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
@@ -1290,6 +1296,172 @@ _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
 	return true;
 }
 
+/*
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts their
+ * operator strategies, while changing the operator as appropriate).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value MINVAL, using a transformed >= key
+ * instead of using the original > key makes it safe to include lower-order
+ * scan keys in the insertion scan key (there must be lower-order scan keys
+ * after the skip array).  We will avoid an extra _bt_first to find the first
+ * value in the index > sk_argument -- at least when the first real matching
+ * value in the index happens to be an exact match for the sk_argument value
+ * that we produced here by incrementing the original input key's sk_argument.
+ * (Backwards scans derive the same benefit when they encounter the sentinel
+ * value MAXVAL, by converting the high_compare key from < to <=.)
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01'.
+ *
+ * Note: We can safely modify array->low_compare/array->high_compare in place
+ * because they just point to copies of our scan->keyData[] input scan keys
+ * (namely the copies returned by _bt_preprocess_array_keys to be used as
+ * input into the standard preprocessing steps in _bt_preprocess_keys).
+ * Everything will be reset if there's a rescan.
+ */
+static void
+_bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+						   BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	/*
+	 * Called last among all preprocessing steps, when the skip array's final
+	 * low_compare and high_compare have both been chosen
+	 */
+	Assert(so->skipScan);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1 && !array->null_elem && array->sksup);
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skiparray_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skiparray_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1825,6 +1997,15 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && array->sksup &&
+						!array->null_elem)
+						_bt_skiparray_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 15dde752f..fa4517e2d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2589,6 +2589,27 @@ ORDER BY thousand;
         1 |     1001
 (1 row)
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+                                            QUERY PLAN                                            
+--------------------------------------------------------------------------------------------------
+ Limit
+   ->  Index Only Scan using tenk1_thous_tenthous on tenk1
+         Index Cond: ((thousand > '-1'::integer) AND (tenthous = ANY ('{1001,3000}'::integer[])))
+(3 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+        1 |     1001
+(2 rows)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index 40ba3d65b..e8c16959a 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -993,6 +993,16 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
 ORDER BY thousand;
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
-- 
2.47.2

v28-0002-Add-nbtree-skip-scan-optimizations.patchapplication/octet-stream; name=v28-0002-Add-nbtree-skip-scan-optimizations.patchDownload
From e096cfc6941533e949dde5d56a357c62d349c3ed Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v28 2/5] Add nbtree skip scan optimizations.

Teach nbtree composite index scans to opportunistically skip over
irrelevant sections of composite indexes given a query with an omitted
prefix column.  When nbtree is passed input scan keys derived from a
query predicate "WHERE b = 5", new nbtree preprocessing steps now output
"WHERE a = ANY(<every possible 'a' value>) AND b = 5" scan keys.  That
is, preprocessing generates a "skip array" (along with an associated
scan key) for the omitted column "a", which makes it safe to mark the
scan key on "b" as required to continue the scan.  This is far more
efficient than a traditional full index scan whenever it allows the scan
to skip over many irrelevant leaf pages, by iteratively repositioning
itself using the keys on "a" and "b" together.

A skip array has "elements" that are generated procedurally and on
demand, but otherwise works just like a regular ScalarArrayOp array.
Preprocessing can freely add a skip array before or after any input
ScalarArrayOp arrays.  Index scans with a skip array decide when and
where to reposition the scan using the same approach as any other scan
with array keys.  This design builds on the design for array advancement
and primitive scan scheduling added to Postgres 17 by commit 5bf748b8.

The core B-Tree operator classes on most discrete types generate their
array elements with the help of their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the next
possible indexable value frequently turns out to be the next value
stored in the index.  Opclasses that lack a skip support routine fall
back on having nbtree "increment" (or "decrement") a skip array's
current element by setting the NEXT (or PRIOR) scan key flag, without
directly changing the scan key's sk_argument.  These sentinel values
behave just like any other value from an array -- though they can never
locate equal index tuples (they can only locate the next group of index
tuples containing the next set of non-sentinel values that the scan's
arrays need to advance to).

Inequality scan keys can affect how skip arrays generate their values.
Their range is constrained by the inequalities.  For example, a skip
array on "a" will only use element values 1 and 2 given a qual such as
"WHERE a BETWEEN 1 AND 2 AND b = 66".  A scan using such a skip array
has almost identical performance characteristics to one with the qual
"WHERE a = ANY('{1, 2}') AND b = 66".  The scan will be much faster when
it can be executed as two selective primitive index scans instead of a
single very large index scan that reads many irrelevant leaf pages.
However, the array transformation process won't always lead to improved
performance at runtime.  Much depends on physical index characteristics.

B-Tree preprocessing is optimistic about skipping working out: it
applies static, generic rules when determining where to generate skip
arrays, which assumes that the runtime overhead of maintaining skip
arrays will pay for itself -- or lead to only a modest performance loss.
As things stand, these assumptions are much too optimistic: skip array
maintenance will lead to unacceptable regressions with unsympathetic
queries (queries whose scan has little to no chance of avoid reading
irrelevant leaf pages).  An upcoming commit will address the problems in
this area by adding a mechanism that greatly lowers the cost of array
maintenance in these unfavorable cases.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |   3 +-
 src/include/access/nbtree.h                   |  35 +-
 src/include/catalog/pg_amproc.dat             |  22 +
 src/include/catalog/pg_proc.dat               |  27 +
 src/include/utils/skipsupport.h               |  98 +++
 src/backend/access/index/indexam.c            |   3 +-
 src/backend/access/nbtree/nbtcompare.c        | 273 +++++++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 600 ++++++++++++--
 src/backend/access/nbtree/nbtree.c            | 187 ++++-
 src/backend/access/nbtree/nbtsearch.c         | 111 ++-
 src/backend/access/nbtree/nbtutils.c          | 754 ++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |   4 +
 src/backend/commands/opclasscmds.c            |  25 +
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/date.c                  |  46 ++
 src/backend/utils/adt/meson.build             |   1 +
 src/backend/utils/adt/selfuncs.c              | 450 ++++++++---
 src/backend/utils/adt/skipsupport.c           |  61 ++
 src/backend/utils/adt/timestamp.c             |  48 ++
 src/backend/utils/adt/uuid.c                  |  70 ++
 doc/src/sgml/btree.sgml                       |  34 +-
 doc/src/sgml/indexam.sgml                     |   3 +-
 doc/src/sgml/indices.sgml                     |  49 +-
 doc/src/sgml/monitoring.sgml                  |   4 +-
 doc/src/sgml/perform.sgml                     |  31 +
 doc/src/sgml/xindex.sgml                      |  16 +-
 src/test/regress/expected/alter_generic.out   |  10 +-
 src/test/regress/expected/btree_index.out     |  41 +
 src/test/regress/expected/create_index.out    | 183 ++++-
 src/test/regress/expected/psql.out            |   3 +-
 src/test/regress/sql/alter_generic.sql        |   5 +-
 src/test/regress/sql/btree_index.sql          |  21 +
 src/test/regress/sql/create_index.sql         |  63 +-
 src/tools/pgindent/typedefs.list              |   3 +
 34 files changed, 2922 insertions(+), 363 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c4a073773..52916bab7 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -214,7 +214,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index c9bc82eba..9c8f7d838 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -707,6 +708,10 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
  *	(BTOPTIONS_PROC).  These procedures define a set of user-visible
  *	parameters that can be used to control operator class behavior.  None of
  *	the built-in B-Tree operator classes currently register an "options" proc.
+ *
+ *	To facilitate more efficient B-Tree skip scans, an operator class may
+ *	choose to offer a sixth amproc procedure (BTSKIPSUPPORT_PROC).  For full
+ *	details, see src/include/utils/skipsupport.h.
  */
 
 #define BTORDER_PROC		1
@@ -714,7 +719,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1027,10 +1033,21 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
-	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+	int			cur_elem;		/* index of current element in elem_values */
+
+	/* fields for skip arrays, which generate element datums procedurally */
+	int16		attlen;			/* attr len in bytes */
+	bool		attbyval;		/* as FormData_pg_attribute.attbyval */
+	bool		null_elem;		/* lowest/highest element actually NULL? */
+	SkipSupport sksup;			/* skip support (NULL if opclass lacks it) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1042,6 +1059,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* arrays might be ahead of scan's key space? */
 	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
@@ -1119,6 +1137,15 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on column without input = */
+
+/* SK_BT_SKIP-only flags (set and unset by array advancement) */
+#define SK_BT_MINVAL	0x00080000	/* invalid sk_argument, use low_compare */
+#define SK_BT_MAXVAL	0x00100000	/* invalid sk_argument, use high_compare */
+#define SK_BT_NEXT		0x00200000	/* positions the scan > sk_argument */
+#define SK_BT_PRIOR		0x00400000	/* positions the scan < sk_argument */
+
+/* Remaps pg_index flag bits to uppermost SK_BT_* byte */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1165,7 +1192,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 19100482b..37000639f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8f..74b7ba711 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2285,6 +2300,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4475,6 +4493,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6354,6 +6375,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9402,6 +9426,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..bc51847cf
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining groups of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * It usually isn't feasible to implement skip support for an opclass whose
+ * input type is continuous.  The B-Tree code falls back on next-key sentinel
+ * values for any opclass that doesn't provide its own skip support function.
+ * This isn't really an implementation restriction; there is no benefit to
+ * providing skip support for an opclass where guessing that the next indexed
+ * value is the next possible indexable value never (or hardly ever) works out.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order)
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 * Operator class decrement/increment functions will never be called with
+	 * a NULL "existing" argument, either.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern SkipSupport PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+												 bool reverse);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 55ec4c103..219df1971 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -489,7 +489,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	if (parallel_aware &&
 		indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 291cb8fc1..4da5a3c1d 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 38a87af1c..bd993b478 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -45,8 +45,15 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
+static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
+								 ScanKey skey, FmgrInfo *orderproc,
+								 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
+								 BTArrayKeyInfo *array, bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
+							   int *numSkipArrayKeys);
 static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
 									  Oid elemtype, StrategyNumber strat,
 									  Datum *elems, int nelems);
@@ -89,6 +96,8 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -101,10 +110,16 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never generated skip array scan keys, it would be possible for "gaps"
+ * to appear that make it unsafe to mark any subsequent input scan keys
+ * (copied from scan->keyData[]) as required to continue the scan.  Prior to
+ * Postgres 18, a qual like "WHERE y = 4" always required a full index scan.
+ * It'll now become "WHERE x = ANY('{every possible x value}') and y = 4" on
+ * output, due to the addition of a skip array on "x".  This has the potential
+ * to be much more efficient than a full index scan (though it'll behave like
+ * a full index scan when there are many distinct values for "x").
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -141,7 +156,12 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -200,6 +220,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProcs[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -231,6 +259,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
+		Assert(!so->skipScan);
 
 		return;
 	}
@@ -288,7 +317,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -345,7 +375,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -393,6 +422,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, _bt_preprocess_array_keys has already added "=" keys
+			 * sufficient to form an unbroken series of "=" constraints on all
+			 * attrs prior to the attr from the final scan->keyData[] key.
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -481,6 +515,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -489,6 +524,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -508,8 +544,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -803,6 +837,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -812,6 +849,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -885,6 +938,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -978,24 +1032,55 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
  * Compare an array scan key to a scalar scan key, eliminating contradictory
  * array elements such that the scalar scan key becomes redundant.
  *
+ * If the opfamily is incomplete we may not be able to determine which
+ * elements are contradictory.  When we return true we'll have validly set
+ * *qual_ok, guaranteeing that at least the scalar scan key can be considered
+ * redundant.  We return false if the comparison could not be made (caller
+ * must keep both scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar scan keys.
+ */
+static bool
+_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
+							   bool *qual_ok)
+{
+	Assert(arraysk->sk_attno == skey->sk_attno);
+	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
+		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
+	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
+		   skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Just call the appropriate helper function based on whether it's a SAOP
+	 * array or a skip array.  Both helpers will set *qual_ok in passing.
+	 */
+	if (array->num_elems != -1)
+		return _bt_saoparray_shrink(scan, arraysk, skey, orderproc, array,
+									qual_ok);
+	else
+		return _bt_skiparray_shrink(scan, skey, array, qual_ok);
+}
+
+/*
+ * Preprocessing of SAOP (non-skip) array scan key, used to determine which
+ * array elements are eliminated as contradictory by a non-array scalar key.
+ * _bt_compare_array_scankey_args helper function.
+ *
  * Array elements can be eliminated as contradictory when excluded by some
  * other operator on the same attribute.  For example, with an index scan qual
  * "WHERE a IN (1, 2, 3) AND a < 2", all array elements except the value "1"
  * are eliminated, and the < scan key is eliminated as redundant.  Cases where
  * every array element is eliminated by a redundant scalar scan key have an
  * unsatisfiable qual, which we handle by setting *qual_ok=false for caller.
- *
- * If the opfamily doesn't supply a complete set of cross-type ORDER procs we
- * may not be able to determine which elements are contradictory.  If we have
- * the required ORDER proc then we return true (and validly set *qual_ok),
- * guaranteeing that at least the scalar scan key can be considered redundant.
- * We return false if the comparison could not be made (caller must keep both
- * scan keys when this happens).
  */
 static bool
-_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
-							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
-							   bool *qual_ok)
+_bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+					 FmgrInfo *orderproc, BTArrayKeyInfo *array, bool *qual_ok)
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
@@ -1006,14 +1091,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
-	Assert(arraysk->sk_attno == skey->sk_attno);
 	Assert(array->num_elems > 0);
-	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
-		   arraysk->sk_strategy == BTEqualStrategyNumber);
-	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
-		   skey->sk_strategy != BTEqualStrategyNumber);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
 
 	/*
 	 * _bt_binsrch_array_skey searches an array for the entry best matching a
@@ -1112,6 +1191,105 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	return true;
 }
 
+/*
+ * Preprocessing of skip (non-SAOP) array scan key, used to determine
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function.
+ *
+ * Unlike _bt_saoparray_shrink, we don't modify caller's array in-place.  Skip
+ * arrays work by procedurally generating their elements as needed, so we just
+ * store the inequality as the skip array's low_compare or high_compare.  The
+ * array's elements will be generated from the range of values that satisfies
+ * both low_compare and high_compare.
+ */
+static bool
+_bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
+					 bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Array's index attribute will be constrained by a strict operator/key.
+	 * Array must not "contain a NULL element" (i.e. the scan must not apply
+	 * "IS NULL" qual when it reaches the end of the index that stores NULLs).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Consider if we should treat caller's scalar scan key as the skip
+	 * array's high_compare or low_compare.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way the scan can always safely assume that
+	 * it's okay to use the input-opclass-only-type proc from so->orderProcs[]
+	 * (they can be cross-type with SAOP arrays, but never with skip arrays).
+	 *
+	 * This approach is enabled by MINVAL/MAXVAL sentinel key markings, which
+	 * can be thought of as representing either the lowest or highest matching
+	 * array element (excluding the NULL element, where applicable, though as
+	 * just discussed it isn't applicable to this range skip array anyway).
+	 * Array keys marked MINVAL/MAXVAL never have a valid datum in their
+	 * sk_argument field.  The scan directly applies the array's low_compare
+	 * key when it encounters MINVAL in the array key proper (just as it
+	 * applies high_compare when it sees MAXVAL set in the array key proper).
+	 * The scan must never use the array's so->orderProcs[] proc against
+	 * low_compare's/high_compare's sk_argument, either (so->orderProcs[] is
+	 * only intended to be used with rhs datums from the array proper/index).
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* replace existing high_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing high_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's high_compare */
+			array->high_compare = skey;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* replace existing low_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing low_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's low_compare */
+			array->low_compare = skey;
+			break;
+		case BTEqualStrategyNumber:
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1137,6 +1315,12 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -1152,41 +1336,36 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	Oid			skip_eq_ops[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
-	ScanKey		cur;
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
+	so->skipScan = (numSkipArrayKeys > 0);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys we'll need to generate below
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -1201,18 +1380,20 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
+		ScanKey		inkey = scan->keyData + input_ikey,
+					cur;
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
 		Oid			elemtype;
@@ -1225,21 +1406,113 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		Datum	   *elem_values;
 		bool	   *elem_nulls;
 		int			num_nonnulls;
-		int			j;
+
+		/* set up next output scan key */
+		cur = &arrayKeyData[numArrayKeyData];
+
+		/* Backfill skip arrays for attrs < or <= input key's attr? */
+		while (numSkipArrayKeys && attno_skip <= inkey->sk_attno)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skip_eq_ops[attno_skip - 1];
+			CompactAttribute *attr;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/*
+				 * Attribute already has an = input key, so don't output a
+				 * skip array for attno_skip.  Just copy attribute's = input
+				 * key into arrayKeyData[] once outside this inner loop.
+				 *
+				 * Note: When we get here there must be a later attribute that
+				 * lacks an equality input key, and still needs a skip array
+				 * (if there wasn't then numSkipArrayKeys would be 0 by now).
+				 */
+				Assert(attno_skip == inkey->sk_attno);
+				/* inkey can't be last input key to be marked required: */
+				Assert(input_ikey < scan->numberOfKeys - 1);
+#if 0
+				/* Could be a redundant input scan key, so can't do this: */
+				Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+					   (inkey->sk_flags & SK_SEARCHNULL));
+#endif
+
+				attno_skip++;
+				break;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize generic BTArrayKeyInfo fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+
+			/* Initialize skip array specific BTArrayKeyInfo fields */
+			attr = TupleDescCompactAttr(RelationGetDescr(rel), attno_skip - 1);
+			reverse = (indoption[attno_skip - 1] & INDOPTION_DESC) != 0;
+			so->arrayKeys[numArrayKeys].attlen = attr->attlen;
+			so->arrayKeys[numArrayKeys].attbyval = attr->attbyval;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup =
+				PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse);
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * We'll need a 3-way ORDER proc.  Set that up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			numArrayKeys++;
+			numArrayKeyData++;	/* keep this scan key/array */
+
+			/* set up next output scan key */
+			cur = &arrayKeyData[numArrayKeyData];
+
+			/* remember having output this skip array and scan key */
+			numSkipArrayKeys--;
+			attno_skip++;
+		}
 
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
-		*cur = scan->keyData[input_ikey];
+		*cur = *inkey;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -1257,7 +1530,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * all btree operators are strict.
 		 */
 		num_nonnulls = 0;
-		for (j = 0; j < num_elems; j++)
+		for (int j = 0; j < num_elems; j++)
 		{
 			if (!elem_nulls[j])
 				elem_values[num_nonnulls++] = elem_values[j];
@@ -1295,7 +1568,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -1306,7 +1579,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -1323,7 +1596,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -1392,23 +1665,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			origelemtype = elemtype;
 		}
 
-		/*
-		 * And set up the BTArrayKeyInfo data.
-		 *
-		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
-		 * scan_key field later on, after so->keyData[] has been finalized.
-		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		/* Initialize generic BTArrayKeyInfo fields */
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
+
+		/* Initialize SAOP array specific BTArrayKeyInfo fields */
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].cur_elem = -1;	/* i.e. invalid */
+
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
+	Assert(numSkipArrayKeys == 0);
+
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set final number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -1514,7 +1788,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -1575,6 +1850,189 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit every so->arrayKeys[] entry.
+ *
+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller must add this many
+ * skip arrays to each of the most significant attributes lacking any keys
+ * that use the = strategy (IS NULL keys count as = keys here).  The specific
+ * attributes that need skip arrays are indicated by initializing caller's
+ * skip_eq_ops[] 0-based attribute offset to a valid = op strategy Oid.  We'll
+ * only ever set skip_eq_ops[] entries to InvalidOid for attributes that
+ * already have an equality key in scan->keyData[] input keys -- and only when
+ * there's some later "attribute gap" for us to "fill-in" with a skip array.
+ *
+ * We're optimistic about skipping working out: we always add exactly the skip
+ * arrays needed to maximize the number of input scan keys that can ultimately
+ * be marked as required to continue the scan (but no more).  For a composite
+ * index on (a, b, c, d), we'll instruct caller to add skip arrays as follows:
+ *
+ * Input keys						Output keys (after all preprocessing)
+ * ----------						-------------------------------------
+ * a = 1							a = 1 (no skip arrays)
+ * a = ANY('{0, 1}')				a = ANY('{0, 1}') (no skip arrays)
+ * b = 42							skip a AND b = 42
+ * b = ANY('{40, 42}')				skip a AND b = ANY('{40, 42}')
+ * a = 1 AND b = 42					a = 1 AND b = 42 (no skip arrays)
+ * a >= 1 AND b = 42				range skip a AND b = 42
+ * a = 1 AND b > 42					a = 1 AND b > 42 (no skip arrays)
+ * a >= 1 AND a <= 3 AND b = 42		range skip a AND b = 42
+ * a = 1 AND c <= 27				a = 1 AND skip b AND c <= 27
+ * a = 1 AND d >= 1					a = 1 AND skip b AND skip c AND d >= 1
+ * a = 1 AND b >= 42 AND d > 1		a = 1 AND range skip b AND skip c AND d > 1
+ */
+static int
+_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_skip = 1,
+				attno_inkey = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+#ifdef DEBUG_DISABLE_SKIP_SCAN
+	/* don't attempt to add skip arrays */
+	return numArrayKeys;
+#endif
+
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+			/* Look up input opclass's equality operator (might fail) */
+			skip_eq_ops[attno_skip - 1] =
+				get_opfamily_member(opfamily, opcintype, opcintype,
+									BTEqualStrategyNumber);
+			if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key
+		 * (doesn't matter whether that attribute has an equality key, or just
+		 * uses inequality keys).
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys
+			 */
+			if (attno_has_equal)
+			{
+				/* Attributes with an = key must have InvalidOid eq_op set */
+				skip_eq_ops[attno_skip - 1] = InvalidOid;
+			}
+			else
+			{
+				Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+				Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+				/* Look up input opclass's equality operator (might fail) */
+				skip_eq_ops[attno_skip - 1] =
+					get_opfamily_member(opfamily, opcintype, opcintype,
+										BTEqualStrategyNumber);
+
+				if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+				{
+					/*
+					 * Input opclass lacks an equality strategy operator, so
+					 * don't generate a skip array that definitely won't work
+					 */
+					break;
+				}
+
+				/* plan on adding a backfill skip array for this attribute */
+				(*numSkipArrayKeys)++;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys have any equality strategy scan
+		 * keys (while counting IS NULL scan keys as equality scan keys)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required later on.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
 /*
  * _bt_find_extreme_element() -- get least or greatest array element
  *
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index c0a8833e0..912062c1f 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -30,6 +30,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -78,11 +79,24 @@ typedef struct BTParallelScanDescData
 	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * The remainder of the space allocated in shared memory is used by scans
+	 * that need to schedule another primitive index scan with skip arrays.
+	 *
+	 * Space holds a flattened representation of each skip array's current
+	 * array element, in datum format.  We don't store BTArrayKeyInfo.cur_elem
+	 * offsets for skip arrays; their elements are determined dynamically.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -335,6 +349,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	else
 		so->keyData = NULL;
 
+	so->skipScan = false;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->oppositeDirCheck = false;
@@ -540,10 +555,158 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume that every input scankey will be output with its
+	 * own SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * array (and an associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		CompactAttribute *attr;
+
+		/* Every skip array needs a space for storing sk_flags */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		attr = TupleDescCompactAttr(rel->rd_att, attnum - 1);
+		if (attr->attlen > 0)
+		{
+			/* Fixed length datum */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  attr->attbyval,
+													  attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * Varlena (or other variable-length) datum.
+		 *
+		 * Assume that serializing the arrays will use just as much space as a
+		 * pass-by-value datum, in addition to space for the largest possible
+		 * index tuple.  This is quite conservative, especially when there are
+		 * multiple varlena columns.  (We could use less memory here, but it
+		 * seems risky to rely on the implementation details that would make
+		 * it safe from this distance.)
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BTMaxItemSize);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   array->attbyval, array->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array key, while freeing memory allocated for old
+		 * sk_argument where required
+		 */
+		if (!array->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -612,6 +775,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -678,14 +842,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -830,6 +989,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -848,12 +1008,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
 	LWLockRelease(&btscan->btps_lock);
 }
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 3fb0a0380..27abb7ef9 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -965,6 +965,15 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  All
+	 * array keys are always considered = keys, but we'll sometimes need to
+	 * treat the current key value as if we were using an inequality strategy.
+	 * This happens whenever a skip array's scan key is found to have been set
+	 * to one of the special sentinel values representing an imaginary point
+	 * before/after some (or all) of the index's real indexable values.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1040,8 +1049,56 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is MINVAL, choose low_compare (when scanning
+				 * backwards it'll be MAXVAL, and we'll choose high_compare).
+				 *
+				 * Note: if the array's low_compare key makes 'chosen' NULL,
+				 * then we behave as if the array's first element is -inf,
+				 * except when !array->null_elem implies a usable NOT NULL
+				 * constraint.
+				 */
+				if (chosen != NULL &&
+					(chosen->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skipequalitykey = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					Assert(so->skipScan);
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MAXVAL));
+						chosen = array->low_compare;
+					}
+					else
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MINVAL));
+						chosen = array->high_compare;
+					}
+
+					Assert(chosen == NULL ||
+						   chosen->sk_attno == skipequalitykey->sk_attno);
+
+					if (!array->null_elem)
+						impliesNN = skipequalitykey;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1084,9 +1141,41 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 
 				/*
-				 * Done if that was the last attribute, or if next key is not
-				 * in sequence (implying no boundary key is available for the
-				 * next attribute).
+				 * If the key that we just added to startKeys[] is a skip
+				 * array = key whose current element is marked NEXT or PRIOR,
+				 * make strat_total > or < (and stop adding boundary keys).
+				 * This can only happen with opclasses that lack skip support.
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(so->skipScan);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(strat_total == BTEqualStrategyNumber);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We're done.  We'll never find an exact = match for a
+					 * NEXT or PRIOR sentinel sk_argument value.  There's no
+					 * sense in trying to add more keys to startKeys[].
+					 */
+					break;
+				}
+
+				/*
+				 * Done if that was the last scan key output by preprocessing.
+				 * Also done if there is a gap index attribute that lacks a
+				 * usable key (only possible when preprocessing was unable to
+				 * generate a skip array key to "fill in the gap").
 				 */
 				if (i >= so->numberOfKeys ||
 					cur->sk_attno != curattr + 1)
@@ -1578,10 +1667,12 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * corresponding value from the last item on the page.  So checking with
 	 * the last item on the page would give a more precise answer.
 	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
+	 * We don't do this for the first page read by each (primitive) scan, to
+	 * avoid slowing down point queries.  They typically don't stand to gain
+	 * much when the optimization can be applied, and are more likely to
+	 * notice the overhead of the precheck.  Also avoid it during skip scans,
+	 * where individual primitive scans that don't just read one leaf page are
+	 * likely to advance their skip array(s) several times per leaf page read.
 	 *
 	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
 	 * just set a low-order required array's key to the best available match
@@ -1605,7 +1696,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !so->skipScan && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 4e455a66b..49afe779f 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -30,6 +30,17 @@
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									  int32 set_elem_result, Datum tupdatum, bool tupnull);
+static void _bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_array_set_low_or_high(Relation rel, ScanKey skey,
+									  BTArrayKeyInfo *array, bool low_not_high);
+static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -207,6 +218,7 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 	int32		result = 0;
 
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
+	Assert(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)));
 
 	if (tupnull)				/* NULL tupdatum */
 	{
@@ -283,6 +295,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* SAOP arrays can't do this */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -405,6 +419,185 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, since skip arrays don't really
+ * contain elements (they generate their array elements procedurally instead).
+ * Our interface matches that of _bt_binsrch_array_skey in every other way.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  We return -1 when tupdatum/tupnull is lower that any value
+ * within the range of the array, and 1 when it is higher than every value.
+ * Caller should pass *set_elem_result to _bt_skiparray_set_element to advance
+ * the array.
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (array->null_elem)
+	{
+		Assert(!array->low_compare && !array->high_compare);
+
+		*set_elem_result = 0;
+		return;
+	}
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+
+	/*
+	 * Assert that any keys that were assumed to be satisfied already (due to
+	 * caller passing cur_elem_trig=true) really are satisfied as expected
+	 */
+#ifdef USE_ASSERT_CHECKING
+	if (cur_elem_trig)
+	{
+		if (ScanDirectionIsForward(dir) && array->low_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												  array->low_compare->sk_collation,
+												  tupdatum,
+												  array->low_compare->sk_argument)));
+
+		if (ScanDirectionIsBackward(dir) && array->high_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												  array->high_compare->sk_collation,
+												  tupdatum,
+												  array->high_compare->sk_argument)));
+	}
+#endif
+}
+
+/*
+ * _bt_skiparray_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Caller passes set_elem_result returned by _bt_binsrch_skiparray_skey for
+ * caller's tupdatum/tupnull.
+ *
+ * We copy tupdatum/tupnull into skey's sk_argument iff set_elem_result == 0.
+ * Otherwise, we set skey to either the lowest or highest value that's within
+ * the range of caller's skip array (whichever is the best available match to
+ * tupdatum/tupnull that is still within the range of the skip array according
+ * to _bt_binsrch_skiparray_skey/set_elem_result).
+ */
+static void
+_bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  int32 set_elem_result, Datum tupdatum, bool tupnull)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (set_elem_result)
+	{
+		/* tupdatum/tupnull is out of the range of the skip array */
+		Assert(!array->null_elem);
+
+		_bt_array_set_low_or_high(rel, skey, array, set_elem_result < 0);
+		return;
+	}
+
+	/* Advance skip array to tupdatum (or tupnull) value */
+	if (unlikely(tupnull))
+	{
+		_bt_skiparray_set_isnull(rel, skey, array);
+		return;
+	}
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen);
+}
+
+/*
+ * _bt_skiparray_set_isnull() -- set skip array scan key to NULL
+ */
+static void
+_bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->null_elem && !array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -414,29 +607,355 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_array_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_array_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  bool low_not_high)
+{
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting array to lowest element (according to low_compare) */
+		skey->sk_flags |= SK_BT_MINVAL;
+	}
+	else
+	{
+		/* Setting array to highest element (according to high_compare) */
+		skey->sk_flags |= SK_BT_MAXVAL;
+	}
+}
+
+/*
+ * _bt_array_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the minimum value within the range
+	 * of a skip array (often just -inf) is never decrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		/* Successfully "decremented" array */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly decrement sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Decrement" from NULL to the high_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->high_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup->decrement(rel, skey->sk_argument, &uflow);
+	if (unlikely(uflow))
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			_bt_skiparray_set_isnull(rel, skey, array);
+
+			/* Successfully "decremented" array to NULL */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	/* Successfully decremented array */
+	return true;
+}
+
+/*
+ * _bt_array_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MINVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the maximum value within the range
+	 * of a skip array (often just +inf) is never incrementable
+	 */
+	if (skey->sk_flags & SK_BT_MAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		/* Successfully "incremented" array */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly increment sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Increment" from NULL to the low_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->low_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup->increment(rel, skey->sk_argument, &oflow);
+	if (unlikely(oflow))
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			_bt_skiparray_set_isnull(rel, skey, array);
+
+			/* Successfully "incremented" array to NULL */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	/* Successfully incremented array */
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -452,6 +971,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -461,29 +981,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_array_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_array_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -507,7 +1028,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 }
 
 /*
- * _bt_rewind_nonrequired_arrays() -- Rewind non-required arrays
+ * _bt_rewind_nonrequired_arrays() -- Rewind SAOP arrays not marked required
  *
  * Called when _bt_advance_array_keys decides to start a new primitive index
  * scan on the basis of the current scan position being before the position
@@ -539,10 +1060,15 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
  *
  * Note: _bt_verify_arrays_bt_first is called by an assertion to enforce that
  * everybody got this right.
+ *
+ * Note: In practice almost all SAOP arrays are marked required during
+ * preprocessing (if necessary by generating skip arrays).  It is hardly ever
+ * truly necessary to call here, but consistently doing so is simpler.
  */
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -550,7 +1076,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -562,16 +1087,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_array_set_low_or_high(rel, cur, array,
+								  ScanDirectionIsForward(dir));
 	}
 }
 
@@ -696,9 +1215,77 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (likely(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))))
+		{
+			/* Scankey has a valid/comparable sk_argument value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0)
+			{
+				/*
+				 * Interpret result in a way that takes NEXT/PRIOR into
+				 * account
+				 */
+				if (cur->sk_flags & SK_BT_NEXT)
+					result = -1;
+				else if (cur->sk_flags & SK_BT_PRIOR)
+					result = 1;
+
+				Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP));
+			}
+		}
+		else
+		{
+			BTArrayKeyInfo *array = NULL;
+
+			/*
+			 * Current array element/array = scan key value is a sentinel
+			 * value that represents the lowest (or highest) possible value
+			 * that's still within the range of the array.
+			 *
+			 * Like _bt_first, we only see MINVAL keys during forwards scans
+			 * (and similarly only see MAXVAL keys during backwards scans).
+			 * Even if the scan's direction changes, we'll stop at some higher
+			 * order key before we can ever reach any MAXVAL (or MINVAL) keys.
+			 * (However, unlike _bt_first we _can_ get to keys marked either
+			 * NEXT or PRIOR, regardless of the scan's current direction.)
+			 */
+			Assert(ScanDirectionIsForward(dir) ?
+				   !(cur->sk_flags & SK_BT_MAXVAL) :
+				   !(cur->sk_flags & SK_BT_MINVAL));
+
+			/*
+			 * There are no valid sk_argument values in MINVAL/MAXVAL keys.
+			 * Check if tupdatum is within the range of skip array instead.
+			 */
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			_bt_binsrch_skiparray_skey(false, dir, tupdatum, tupnull,
+									   array, cur, &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum satisfies both low_compare and high_compare, so
+				 * it's time to advance the array keys.
+				 *
+				 * Note: It's possible that the skip array will "advance" from
+				 * its MINVAL (or MAXVAL) representation to an alternative,
+				 * logically equivalent representation of the same value: a
+				 * representation where the = key gets a valid datum in its
+				 * sk_argument.  This is only possible when low_compare uses
+				 * the >= strategy (or high_compare uses the <= strategy).
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1017,18 +1604,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1053,18 +1631,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -1080,14 +1649,22 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			bool		cur_elem_trig = (sktrig_required && ikey == sktrig);
 
 			/*
-			 * Binary search for closest match that's available from the array
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems == -1)
+				_bt_binsrch_skiparray_skey(cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * Binary search for the closest match from the SAOP array
+			 */
+			else
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 		}
 		else
 		{
@@ -1163,11 +1740,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+		if (array)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->num_elems == -1)
+			{
+				/* Skip array's new element is tupdatum (or MINVAL/MAXVAL) */
+				_bt_skiparray_set_element(rel, cur, array, result,
+										  tupdatum, tupnull);
+			}
+			else if (array->cur_elem != set_elem)
+			{
+				/* SAOP array's new element is set_elem datum */
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
 		}
 	}
 
@@ -1586,10 +2173,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -1920,6 +2508,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key uses one of several sentinel values.  We just
+		 * fall back on _bt_tuple_before_array_skeys when we see such a value.
+		 */
+		if (key->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL |
+							 SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index dd6f5a15c..817ad358f 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -106,6 +106,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 8546366ee..a6dd8eab5 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("ordering equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (GetIndexAmRoutineByAmId(amoid, false)->amcanhash)
 	{
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index f279853de..4227ab1a7 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f23cfad71..244f48f4f 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 5b35debc8..7033b1c37 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -94,7 +94,6 @@
 
 #include "postgres.h"
 
-#include <ctype.h>
 #include <math.h>
 
 #include "access/brin.h"
@@ -193,6 +192,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -214,6 +215,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5943,6 +5946,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -7001,6 +7090,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -7010,17 +7146,22 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
+	double		correlation = 0;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
 	int			indexcol;
+	bool		set_correlation = false;
 	bool		eqQualHere;
-	bool		found_saop;
+	bool		found_array;
+	bool		found_rowcompare;
 	bool		found_is_null_op;
+	bool		upper_inequal_col;
+	bool		lower_inequal_col;
 	double		num_sa_scans;
+	double		upperselectivity;
+	double		lowerselectivity;
 	ListCell   *lc;
 
 	/*
@@ -7031,21 +7172,33 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * it's OK to count them in indexSelectivity, but they should not count
 	 * for estimating numIndexTuples.  So we must examine the given indexquals
 	 * to find out which ones count as boundary quals.  We rely on the
-	 * knowledge that they are given in index column order.
+	 * knowledge that they are given in index column order (though this
+	 * process is complicated by the use of skip arrays, as explained below).
 	 *
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
+	 * If there's a ScalarArrayOp array in the quals, or if B-Tree
+	 * preprocessing will be able to generate a skip array, we'll actually
+	 * perform up to N index descents (not just one), but the underlying
 	 * operator can be considered to act the same as it normally does.
+	 *
+	 * In practice, non-leading quals often _can_ act as boundary quals due to
+	 * preprocessing generating a "bridging" skip array.  Whether or not we'll
+	 * actually treat lower-order quals as boundary quals (that is, quals that
+	 * influence our numIndexTuples estimate) is determined by heuristics.
 	 */
 	indexBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
-	found_saop = false;
+	found_array = false;
+	found_rowcompare = false;
 	found_is_null_op = false;
+	upper_inequal_col = false;
+	lower_inequal_col = false;
 	num_sa_scans = 1;
+	upperselectivity = 1.0;
+	lowerselectivity = 1.0;
 	foreach(lc, path->indexclauses)
 	{
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
@@ -7054,12 +7207,120 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		if (indexcol != iclause->indexcol)
 		{
 			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
-			eqQualHere = false;
-			indexcol++;
+			if (eqQualHere)
+				indexcol++;		/* don't skip the previous '=' qual's column */
+			else if (found_rowcompare)
+				break;			/* Skip arrays can't come after a RowCompare */
+
+			/*
+			 * Consider whether nbtree preprocessing will backfill skip arrays
+			 * for index columns lacking an equality clause, and account for
+			 * the cost of maintaining those skip arrays
+			 */
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct = DEFAULT_NUM_DISTINCT,
+							new_num_sa_scans;
+				bool		isdefault = true;
+
+				found_array = true;
+
+				/*
+				 * Now estimate number of "array elements" using ndistinct.
+				 *
+				 * Internally, nbtree treats skip scans as scans with SAOP
+				 * style arrays that generate elements procedurally.  This is
+				 * like a "col = ANY('{every possible col value}')" qual.
+				 */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				if (HeapTupleIsValid(vardata.statsTuple))
+				{
+					ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+					if (indexcol == 0)
+					{
+						/*
+						 * Get an estimate of the leading column's correlation
+						 * in passing (avoids rereading variable stats below)
+						 */
+						Assert(!set_correlation);
+						correlation = btcost_correlation(index, &vardata);
+						set_correlation = true;
+					}
+				}
+
+				ReleaseVariableStats(vardata);
+
+				/*
+				 * Apply the selectivities of any inequalities to ndistinct
+				 * iff there was a non-equality clause for this column and we
+				 * don't just have a default ndistinct estimate
+				 */
+				if ((upper_inequal_col || lower_inequal_col) && !isdefault)
+				{
+					double		ndistinctfrac = 1.0;
+
+					if (upper_inequal_col)
+						ndistinctfrac -= (1.0 - upperselectivity);
+					if (lower_inequal_col)
+						ndistinctfrac -= (1.0 - lowerselectivity);
+
+					CLAMP_PROBABILITY(ndistinctfrac);
+					ndistinct = rint(ndistinct * ndistinctfrac);
+					ndistinct = Max(ndistinct, 1);
+				}
+
+				/*
+				 * Account for possible +inf element, used to find the highest
+				 * item in the index when qual lacks a < or <= upper bound
+				 */
+				if (!upper_inequal_col)
+					ndistinct += 1;
+
+				/*
+				 * Account for possible -inf element, used to find the lowest
+				 * item in the index when qual lacks a > or >= lower bound
+				 */
+				if (!lower_inequal_col)
+					ndistinct += 1;
+
+				/* Forget about any upper_inequal_col/lower_inequal_col */
+				upper_inequal_col = false;
+				lower_inequal_col = false;
+
+				/*
+				 * Multiply our running estimate by ndistinct to update it.
+				 * Here we make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * using skip scan.
+				 */
+				new_num_sa_scans = num_sa_scans * ndistinct;
+
+				/*
+				 * Stop adding new skip arrays when the would-be new
+				 * num_sa_scans exceeds the total number of index pages
+				 */
+				if (index->pages < new_num_sa_scans)
+				{
+					/* Qual (and later quals) won't affect numIndexTuples */
+					break;
+				}
+
+				/* Done counting skip array "elements" for this column */
+				num_sa_scans = new_num_sa_scans;
+				indexcol++;
+			}
+
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+				break;			/* no quals at all for indexcol (can't skip) */
+
+			/* reset for next indexcol */
+			eqQualHere = false;
+			upper_inequal_col = false;
+			lower_inequal_col = false;
+			upperselectivity = 1.0;
+			lowerselectivity = 1.0;
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -7081,6 +7342,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_rowcompare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -7089,7 +7351,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				double		alength = estimate_array_length(root, other_operand);
 
 				clause_op = saop->opno;
-				found_saop = true;
+				found_array = true;
 				/* estimate SA descents by indexBoundQuals only */
 				if (alength > 1)
 					num_sa_scans *= alength;
@@ -7101,7 +7363,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skip scan purposes */
 					eqQualHere = true;
 				}
 			}
@@ -7117,6 +7379,37 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				Assert(op_strategy != 0);	/* not a member of opfamily?? */
 				if (op_strategy == BTEqualStrategyNumber)
 					eqQualHere = true;
+
+				if (!eqQualHere && !found_rowcompare &&
+					indexcol < index->nkeycolumns - 1)
+				{
+					double		selec;
+
+					/*
+					 * Skip scan requires tracking inequality selectivities to
+					 * compute an adjusted whole-column ndistinct.  Set things
+					 * up now (will be used when we move onto the next clause
+					 * against some later index column)
+					 *
+					 * Like clauselist_selectivity, we recognize redundant
+					 * inequalities such as "x < 4 AND x < 5"; only the
+					 * tighter constraint will be counted.
+					 */
+					selec = (double) clause_selectivity(root, (Node *) rinfo,
+														0, JOIN_INNER, NULL);
+					if (op_strategy < BTEqualStrategyNumber)
+					{
+						if (selec < upperselectivity)
+							upperselectivity = selec;
+						upper_inequal_col = true;
+					}
+					else
+					{
+						if (selec < lowerselectivity)
+							lowerselectivity = selec;
+						lower_inequal_col = true;
+					}
+				}
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
@@ -7126,13 +7419,13 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	/*
 	 * If index is unique and we found an '=' clause for each column, we can
 	 * just assume numIndexTuples = 1 and skip the expensive
-	 * clauselist_selectivity calculations.  However, a ScalarArrayOp or
-	 * NullTest invalidates that theory, even though it sets eqQualHere.
+	 * clauselist_selectivity calculations.  However, an array or NullTest
+	 * invalidates that theory, even though it sets eqQualHere.
 	 */
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
-		!found_saop &&
+		!found_array &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
 	else
@@ -7154,11 +7447,11 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		numIndexTuples = btreeSelectivity * index->rel->tuples;
 
 		/*
-		 * btree automatically combines individual ScalarArrayOpExpr primitive
-		 * index scans whenever the tuples covered by the next set of array
-		 * keys are close to tuples covered by the current set.  That puts a
-		 * natural ceiling on the worst case number of descents -- there
-		 * cannot possibly be more than one descent per leaf page scanned.
+		 * btree automatically combines individual array primitive index scans
+		 * whenever the tuples covered by the next set of array keys are close
+		 * to tuples covered by the current set.  That puts a natural ceiling
+		 * on the worst case number of descents -- there cannot possibly be
+		 * more than one descent per leaf page scanned.
 		 *
 		 * Clamp the number of descents to at most 1/3 the number of index
 		 * pages.  This avoids implausibly high estimates with low selectivity
@@ -7172,16 +7465,18 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * of leaf pages (we make it 1/3 the total number of pages instead) to
 		 * give the btree code credit for its ability to continue on the leaf
 		 * level with low selectivity scans.
+		 *
+		 * Note: num_sa_scans includes both ScalarArrayOp array elements and
+		 * skip array elements whose qual affects our numIndexTuples estimate.
 		 */
 		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
-		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
-		 * to integer.
+		 * As in genericcostestimate(), we have to adjust for any array quals
+		 * included in indexBoundQuals, and then round to integer.
 		 *
-		 * It is tempting to make genericcostestimate behave as if SAOP
+		 * It is tempting to make genericcostestimate behave as if array
 		 * clauses work in almost the same way as scalar operators during
 		 * btree scans, making the top-level scan look like a continuous scan
 		 * (as opposed to num_sa_scans-many primitive index scans).  After
@@ -7234,110 +7529,25 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * cost is somewhat arbitrarily set at 50x cpu_operator_cost per page
 	 * touched.  The number of such pages is btree tree height plus one (ie,
 	 * we charge for the leaf page too).  As above, charge once per estimated
-	 * SA index descent.
+	 * index descent.
 	 */
 	descentCost = (index->tree_height + 1) * DEFAULT_PAGE_CPU_MULTIPLIER * cpu_operator_cost;
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!set_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* get_variable_index_correlation called earlier */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..2bd35d2d2
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns skip support struct, allocating in caller's memory
+ * context.  Otherwise returns NULL, indicating that operator class has no
+ * skip support function.
+ */
+SkipSupport
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse)
+{
+	Oid			skipSupportFunction;
+	SkipSupport sksup;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return NULL;
+
+	sksup = palloc(sizeof(SkipSupportData));
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return sksup;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 9682f9dbd..347089b76 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2304,6 +2305,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 4f8402ef9..1b72059d2 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,6 +13,7 @@
 
 #include "postgres.h"
 
+#include <limits.h>
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
@@ -21,6 +22,7 @@
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -416,6 +418,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..0a6a48749 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value stored
+     in an index, so the domain of the particular data type stored within the
+     index must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index 768b77aa0..d5adb58c1 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -835,7 +835,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index aaa6586d3..e6a0b7093 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4249,7 +4249,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage
      <replaceable>column_name</replaceable> =
      <replaceable>value2</replaceable> ...</literal> construct, though only
     when the optimizer transforms the construct into an equivalent
-    multi-valued array representation.
+    multi-valued array representation.  Similarly, when B-Tree index scans use
+    the skip scan strategy, an index search is performed each time the scan is
+    repositioned to the next index leaf page that might have matching tuples.
    </para>
   </note>
   <tip>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index e6146c113..8918a2c16 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -860,6 +860,37 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE thousand IN (1, 2, 3, 4);
     <structname>tenk1_thous_tenthous</structname> index leaf page.
    </para>
 
+   <para>
+    The <quote>Index Searches</quote> line is also useful with B-tree index
+    scans that apply the <firstterm>skip scan</firstterm> optimization to
+    more efficiently traverse through an index:
+<screen>
+EXPLAIN ANALYZE SELECT four, unique1 FROM tenk1 WHERE four BETWEEN 1 AND 3 AND unique1 = 42;
+                                                              QUERY PLAN
+---------------------------------------------------------------------&zwsp;-----------------------------------------------------------------
+ Index Only Scan using tenk1_four_unique1_idx on tenk1  (cost=0.29..6.90 rows=1 width=8) (actual time=0.006..0.007 rows=1.00 loops=1)
+   Index Cond: ((four &gt;= 1) AND (four &lt;= 3) AND (unique1 = 42))
+   Heap Fetches: 0
+   Index Searches: 3
+   Buffers: shared hit=7
+ Planning Time: 0.029 ms
+ Execution Time: 0.012 ms
+</screen>
+
+    Here we see an Index-Only Scan node using
+    <structname>tenk1_four_unique1_idx</structname>, a composite index on the
+    <structname>tenk1</structname> table's <structfield>four</structfield> and
+    <structfield>unique1</structfield> columns.  The scan performs 3 searches
+    that each read a single index leaf page:
+    <quote><literal>four = 1 AND unique1 = 42</literal></quote>,
+    <quote><literal>four = 2 AND unique1 = 42</literal></quote>, and
+    <quote><literal>four = 3 AND unique1 = 42</literal></quote>.  This index
+    is generally a good target for skip scan, since its leading column (the
+    <structfield>four</structfield> column) contains only 4 distinct values,
+    while its second/final column (the <structfield>unique1</structfield>
+    column) contains many distinct values.
+   </para>
+
    <para>
     Another type of extra information is the number of rows removed by a
     filter condition:
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 053619624..7e23a7b6e 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index 0c274d56a..23bf33f10 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  ordering equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 8879554c3..bfb1a286e 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -581,6 +581,47 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Index Only Scan using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Index Only Scan Backward using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 --
 -- Test for multilevel page deletion
 --
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index bd5f002cf..15dde752f 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1637,7 +1637,9 @@ DROP TABLE syscol_table;
 -- Tests for IS NULL/IS NOT NULL with b-tree indexes
 --
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 SET enable_seqscan = OFF;
 SET enable_indexscan = ON;
@@ -1645,7 +1647,7 @@ SET enable_bitmapscan = ON;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1657,13 +1659,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1678,12 +1680,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1695,13 +1703,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1722,12 +1730,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0,
      1
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc nulls last,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1739,13 +1753,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1760,12 +1774,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2  nulls first,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1777,13 +1797,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1798,6 +1818,12 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 -- Check initial-positioning logic too
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2);
@@ -1829,20 +1855,24 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
 (2 rows)
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-         |        
-     278 |     999
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 5;
+ unique1 |  unique2   
+---------+------------
+     500 |           
+     100 |           
+         |           
+         | 2147483647
+     278 |        999
+(5 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-     278 |     999
-       0 |     998
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 3;
+ unique1 |  unique2   
+---------+------------
+         | 2147483647
+     278 |        999
+       0 |        998
+(3 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
@@ -2247,7 +2277,8 @@ SELECT count(*) FROM dupindexcols
 (1 row)
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 explain (costs off)
 SELECT unique1 FROM tenk1
@@ -2269,7 +2300,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2289,7 +2320,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2309,6 +2340,25 @@ ORDER BY thousand DESC, tenthous DESC;
         0 |     3000
 (2 rows)
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ Index Only Scan Backward using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > 995) AND (tenthous = ANY ('{998,999}'::integer[])))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+ thousand | tenthous 
+----------+----------
+      999 |      999
+      998 |      998
+(2 rows)
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -2339,6 +2389,45 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 ---------
 (0 rows)
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+                   QUERY PLAN                    
+-------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY (NULL::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY ('{NULL,NULL,NULL}'::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: ((unique1 IS NULL) AND (unique1 IS NULL))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+ unique1 
+---------
+(0 rows)
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
                                 QUERY PLAN                                 
@@ -2462,6 +2551,44 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 ---------
 (0 rows)
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > '-1'::integer) AND (thousand >= 0) AND (tenthous = 3000))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+(1 row)
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand < 3) AND (thousand <= 2) AND (tenthous = 1001))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        1 |     1001
+(1 row)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 6543e90de..6ebd6265f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5331,9 +5331,10 @@ Function              | in_range(time without time zone,time without time zone,i
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql
index 670ad5c6e..68c61dbc7 100644
--- a/src/test/regress/sql/btree_index.sql
+++ b/src/test/regress/sql/btree_index.sql
@@ -327,6 +327,27 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 
 --
 -- Test for multilevel page deletion
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index be570da08..40ba3d65b 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -650,7 +650,9 @@ DROP TABLE syscol_table;
 --
 
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 
 SET enable_seqscan = OFF;
@@ -663,6 +665,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -675,6 +678,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NUL
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0, 1);
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -686,6 +690,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -697,6 +702,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -716,9 +722,9 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
   ORDER BY unique2 LIMIT 2;
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 5;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 3;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
 
@@ -852,7 +858,8 @@ SELECT count(*) FROM dupindexcols
   WHERE f1 BETWEEN 'WA' AND 'ZZZ' and id < 1000 and f1 ~<~ 'YX';
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 
 explain (costs off)
@@ -864,7 +871,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -874,7 +881,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -884,6 +891,15 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand DESC, tenthous DESC;
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -897,6 +913,21 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 
 SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('{33, 44}'::bigint[]);
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
 
@@ -942,6 +973,26 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
 SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index dfe2690bd..ff2342046 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -220,6 +220,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2708,6 +2709,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.47.2

v28-0003-Lower-nbtree-skip-array-maintenance-overhead.patchapplication/octet-stream; name=v28-0003-Lower-nbtree-skip-array-maintenance-overhead.patchDownload
From a905465b7bc21a68a64557be8f5afb057959002b Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v28 3/5] Lower nbtree skip array maintenance overhead.

Add an optimization that fixes regressions in index scans that are
nominally eligible to use skip scan, but can never actually benefit from
skipping.  These are cases where a leading prefix column contains many
distinct values -- especially when the number of values approaches the
total number of index tuples, where skipping can never be profitable.

The optimization is activated dynamically, as a fallback strategy.  It
works by determining a prefix of leading index columns whose scan keys
(often skip array scan keys) are guaranteed to be satisfied by every
possible index tuple on a given page.  _bt_readpage is then able to
start comparisons at the first scan key that might not be satisfied.
This necessitates making _bt_readpage temporarily cease maintaining the
scan's arrays.  _bt_checkkeys will treat the scan's keys as if they were
not marked as required during preprocessing.  This process relies on the
non-required SAOP array logic in _bt_advance_array_keys that was added
to Postgres 17 by commit 5bf748b8.

The new optimization does not affect array primitive scan scheduling.
It is similar to the precheck optimization added by Postgres 17 commit
e0b1ee17dc, though it is only used during nbtree scans with skip arrays.
It can be applied during scans that were never eligible for the precheck
optimization.  As a result, many scans that cannot benefit from skipping
will still benefit from using skip arrays (skip arrays indirectly enable
the use of the optimization introduced by this commit).

Skip scan's approach of adding skip arrays during preprocessing and then
fixing (or significantly ameliorating) the resulting regressions seen in
unsympathetic cases is enabled by the optimization added by this commit
(and by the "look ahead" optimization introduced by commit 5bf748b8).
This allows the planner to avoid generating distinct, competing index
paths (one path for skip scan, another for an equivalent traditional
full index scan).  The overall effect is to make scan runtime close to
optimal, even when the planner works off an incorrect cardinality
estimate.  Scans will also perform well given a skipped column with data
skew: individual groups of pages with many distinct values in respect of
a skipped column can be read about as efficiently as before, without
having to give up on skipping over other provably-irrelevant leaf pages.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h           |   5 +-
 src/backend/access/nbtree/nbtsearch.c |  48 ++++
 src/backend/access/nbtree/nbtutils.c  | 395 +++++++++++++++++++++++---
 3 files changed, 401 insertions(+), 47 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 9c8f7d838..60a0c0e65 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1115,11 +1115,13 @@ typedef struct BTReadPageState
 
 	/*
 	 * Input and output parameters, set and unset by both _bt_readpage and
-	 * _bt_checkkeys to manage precheck optimizations
+	 * _bt_checkkeys to manage precheck and forcenonrequired optimizations
 	 */
 	bool		firstpage;		/* on first page of current primitive scan? */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
+	bool		forcenonrequired;	/* treat all scan keys as nonrequired? */
+	int			ikey;			/* start comparisons from ikey'th scan key */
 
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
@@ -1328,6 +1330,7 @@ extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arra
 						  IndexTuple tuple, int tupnatts);
 extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
 									 IndexTuple finaltup);
+extern void _bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 27abb7ef9..29ccd9198 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1651,6 +1651,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.firstpage = firstpage;
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
+	pstate.forcenonrequired = false;
+	pstate.ikey = 0;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
@@ -1733,6 +1735,14 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 													   so->currPos.currPage);
 					return false;
 				}
+
+				/*
+				 * Use pstate.ikey optimization during primitive index scans
+				 * with skip arrays when reading a second or subsequent page
+				 * (unless we've reached the rightmost page)
+				 */
+				if (!pstate.firstpage && so->skipScan && minoff < maxoff)
+					_bt_skip_ikeyprefix(scan, &pstate);
 			}
 
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
@@ -1774,6 +1784,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum < pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1837,6 +1848,15 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			IndexTuple	itup = (IndexTuple) PageGetItem(page, iid);
 			int			truncatt;
 
+			if (pstate.forcenonrequired)
+			{
+				Assert(so->skipScan);
+
+				/* recover from treating the scan's keys as nonrequired */
+				_bt_start_array_keys(scan, dir);
+				pstate.forcenonrequired = false;
+				pstate.ikey = 0;
+			}
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
@@ -1873,6 +1893,14 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 													   so->currPos.currPage);
 					return false;
 				}
+
+				/*
+				 * Use pstate.ikey optimization during primitive index scans
+				 * with skip arrays when reading a second or subsequent page
+				 * (unless we've reached the leftmost page)
+				 */
+				if (!pstate.firstpage && so->skipScan && minoff < maxoff)
+					_bt_skip_ikeyprefix(scan, &pstate);
 			}
 
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
@@ -1917,6 +1945,15 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff && pstate.forcenonrequired)
+			{
+				Assert(so->skipScan);
+
+				/* recover from treating the scan's keys as nonrequired */
+				_bt_start_array_keys(scan, dir);
+				pstate.forcenonrequired = false;
+				pstate.ikey = 0;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
@@ -1928,6 +1965,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum > pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1993,6 +2031,16 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 		so->currPos.itemIndex = MaxTIDsPerBTreePage - 1;
 	}
 
+	/*
+	 * As far as our caller is concerned, the scan's arrays always track its
+	 * progress through the index's key space.
+	 *
+	 * If _bt_skip_ikeyprefix told us to temporarily treat all scan keys as
+	 * nonrequired (during a skip scan), then we must recover afterwards by
+	 * advancing our arrays using finaltup (with !pstate.forcenonrequired).
+	 */
+	Assert(pstate.ikey == 0 && !pstate.forcenonrequired);
+
 	return (so->currPos.firstItem <= so->currPos.lastItem);
 }
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 49afe779f..0ae1e05b9 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -57,11 +57,12 @@ static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool forcenonrequired,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-								 ScanDirection dir, bool *continuescan);
+								 ScanDirection dir, bool forcenonrequired, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -1421,14 +1422,14 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
  * postcondition's <= operator with a >=.  In other words, just swap the
  * precondition with the postcondition.)
  *
- * We also deal with "advancing" non-required arrays here.  Callers whose
- * sktrig scan key is non-required specify sktrig_required=false.  These calls
- * are the only exception to the general rule about always advancing the
+ * We also deal with "advancing" non-required arrays here (or arrays that are
+ * treated as non-required for the duration of a _bt_readpage call).  Callers
+ * whose sktrig scan key is non-required specify sktrig_required=false.  These
+ * calls are the only exception to the general rule about always advancing the
  * required array keys (the scan may not even have a required array).  These
  * callers should just pass a NULL pstate (since there is never any question
  * of stopping the scan).  No call to _bt_tuple_before_array_skeys is required
- * ahead of these calls (it's already clear that any required scan keys must
- * be satisfied by caller's tuple).
+ * ahead of these calls.
  *
  * Note that we deal with non-array required equality strategy scan keys as
  * degenerate single element arrays here.  Obviously, they can never really
@@ -1480,8 +1481,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		pstate->firstmatch = false;
 
-		/* Shouldn't have to invalidate 'prechecked', though */
-		Assert(!pstate->prechecked);
+		/* Shouldn't have to invalidate precheck/forcenonrequired state */
+		Assert(!pstate->prechecked && !pstate->forcenonrequired &&
+			   pstate->ikey == 0);
 
 		/*
 		 * Once we return we'll have a new set of required array keys, so
@@ -1490,6 +1492,26 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		pstate->rechecks = 0;
 		pstate->targetdistance = 0;
 	}
+	else if (sktrig < so->numberOfKeys - 1 &&
+			 !(so->keyData[so->numberOfKeys - 1].sk_flags & SK_SEARCHARRAY))
+	{
+		int			least_sign_ikey = so->numberOfKeys - 1;
+		bool		continuescan;
+
+		/*
+		 * Optimization: perform a precheck of the least significant key
+		 * during !sktrig_required calls when it isn't already our sktrig
+		 * (provided the precheck key is not itself an array).
+		 *
+		 * When the precheck works out we'll avoid an expensive binary search
+		 * of sktrig's array (plus any other arrays before least_sign_ikey).
+		 */
+		Assert(so->keyData[sktrig].sk_flags & SK_SEARCHARRAY);
+		if (!_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
+							   false, false, false, false,
+							   &continuescan, &least_sign_ikey))
+			return false;
+	}
 
 	Assert(_bt_verify_keys_with_arraykeys(scan));
 
@@ -1533,8 +1555,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -1668,7 +1688,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -1710,7 +1730,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -1725,7 +1745,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -1785,6 +1805,12 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * of any required scan key).  All that matters is whether caller's tuple
 	 * satisfies the new qual, so it's safe to just skip the _bt_check_compare
 	 * recheck when we've already determined that it can only return 'false'.
+	 *
+	 * Note: In practice most scan keys are marked required by preprocessing,
+	 * if necessary by generating a preceding skip array.  We nevertheless
+	 * often handle array keys marked required as if they were nonrequired.
+	 * This behavior is requested by our _bt_check_compare caller, though only
+	 * when it is passed "forcenonrequired=true" by _bt_checkkeys.
 	 */
 	if ((sktrig_required && all_required_satisfied) ||
 		(!sktrig_required && all_satisfied))
@@ -1796,7 +1822,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -2040,20 +2066,23 @@ new_prim_scan:
 	 * the scan arrives on the next sibling leaf page) when it has already
 	 * read at least one leaf page before the one we're reading now.  This is
 	 * important when reading subsets of an index with many distinct values in
-	 * respect of an attribute constrained by an array.  It encourages fewer,
-	 * larger primitive scans where that makes sense.
-	 *
-	 * Note: so->scanBehind is primarily used to indicate that the scan
-	 * encountered a finaltup that "satisfied" one or more required scan keys
-	 * on a truncated attribute value/-inf value.  We can safely reuse it to
-	 * force the scan to stay on the leaf level because the considerations are
-	 * exactly the same.
+	 * respect of an attribute constrained by an array (often a skip array).
+	 * It encourages fewer, larger primitive scans where that makes sense.
+	 * This will in turn encourage _bt_readpage to apply the forcenonrequired
+	 * optimization when applicable (i.e. when the scan has a skip array).
 	 *
 	 * Note: This heuristic isn't as aggressive as you might think.  We're
 	 * conservative about allowing a primitive scan to step from the first
 	 * leaf page it reads to the page's sibling page (we only allow it on
 	 * first pages whose finaltup strongly suggests that it'll work out).
 	 * Clearing this first page finaltup hurdle is a strong signal in itself.
+	 *
+	 * Note: so->scanBehind is primarily used to indicate that the scan
+	 * encountered a finaltup that "satisfied" one or more required scan keys
+	 * on a truncated attribute value/-inf value.  We reuse it to force the
+	 * scan to stay on the leaf level because the considerations are just the
+	 * same (the array's are ahead of the index key space, or they're behind
+	 * when we're scanning backwards).
 	 */
 	if (!pstate->firstpage)
 	{
@@ -2229,14 +2258,16 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	TupleDesc	tupdesc = RelationGetDescr(scan->indexRelation);
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ScanDirection dir = so->currPos.dir;
-	int			ikey = 0;
+	int			ikey = pstate->ikey;
 	bool		res;
 
+	Assert(ikey == 0 || pstate->forcenonrequired);
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+							arrayKeys, pstate->forcenonrequired,
+							pstate->prechecked, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
@@ -2247,12 +2278,12 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 *
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
-		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
+		Assert(!pstate->prechecked && !pstate->firstmatch &&
+			   !pstate->forcenonrequired);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
-	if (pstate->prechecked || pstate->firstmatch)
+	if ((pstate->prechecked || pstate->firstmatch) && !pstate->forcenonrequired)
 	{
 		bool		dcontinuescan;
 		int			dikey = 0;
@@ -2262,7 +2293,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, false, false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -2285,6 +2316,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * It's also possible that the scan is still _before_ the _start_ of
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
+	Assert(!pstate->forcenonrequired);
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
@@ -2400,7 +2432,7 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	Assert(so->numArrayKeys);
 
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -2408,6 +2440,231 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	return true;
 }
 
+/*
+ * Determine a prefix of scan keys that are guaranteed to be satisfied by
+ * every possible tuple on pstate's page.  Used during scans with skip arrays,
+ * which do not use the similar optimization controlled by pstate.prechecked.
+ *
+ * Sets pstate.ikey (and pstate.forcenonrequired) on success, making later
+ * calls to _bt_checkkeys start checks of each tuple from the so->keyData[]
+ * entry at pstate.ikey (while treating keys >= pstate.ikey as nonrequired).
+ *
+ * When _bt_checkkeys treats the scan's required keys as non-required, the
+ * scan's array keys won't be properly maintained (they won't have advanced in
+ * lockstep with our progress through the index's key space as expected).
+ * Caller must recover from this by restarting the scan's array keys and
+ * resetting pstate.ikey and pstate.forcenonrequired just ahead of the
+ * _bt_checkkeys call for the page's final tuple (the pstate.finaltup tuple).
+ */
+void
+_bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	ScanDirection dir = so->currPos.dir;
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	ItemId		iid;
+	IndexTuple	firsttup,
+				lasttup;
+	int			ikey = 0,
+				arrayidx = 0,
+				firstchangingattnum;
+
+	Assert(so->skipScan && pstate->minoff < pstate->maxoff);
+
+	/* minoff is an offset to the lowest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->minoff);
+	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* maxoff is an offset to the highest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->maxoff);
+	lasttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* Determine the first attribute whose values change on caller's page */
+	firstchangingattnum = _bt_keep_natts_fast(rel, firsttup, lasttup);
+
+	for (; ikey < so->numberOfKeys; ikey++)
+	{
+		ScanKey		key = so->keyData + ikey;
+		BTArrayKeyInfo *array;
+		Datum		tupdatum;
+		bool		tupnull;
+		int32		result;
+
+		/*
+		 * Determine if it's safe to set pstate.ikey to an offset to a key
+		 * that comes after this key, by examining this key
+		 */
+		if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) == 0)
+		{
+			/*
+			 * This is the first key that is not marked required (only happens
+			 * when _bt_preprocess_keys couldn't mark all keys required due to
+			 * implementation restrictions affecting skip array generation)
+			 */
+			Assert(!(key->sk_flags & SK_BT_SKIP));
+			break;				/* pstate.ikey to be set to nonrequired ikey */
+		}
+		if (key->sk_strategy != BTEqualStrategyNumber)
+		{
+			/*
+			 * This is the scan's first inequality key (barring inequalities
+			 * that are used to describe the range of a = skip array key).
+			 *
+			 * We could handle this like a = key, but it doesn't seem worth
+			 * the trouble.  Have _bt_checkkeys start with this inequality.
+			 */
+			break;				/* pstate.ikey to be set to inequality's ikey */
+		}
+		if (!(key->sk_flags & SK_SEARCHARRAY))
+		{
+			/*
+			 * Found a scalar (non-array) = key.
+			 *
+			 * It is unsafe to set pstate.ikey to an ikey beyond this key,
+			 * unless the = key is satisfied by every possible tuple on the
+			 * page (possible only when attribute has just one distinct value
+			 * among all tuples on the page).
+			 */
+			if (key->sk_attno < firstchangingattnum)
+			{
+				/*
+				 * Only one distinct value on the page for this key's column.
+				 * Must make sure that = key is actually satisfied by the
+				 * value that is stored within every tuple on the page.
+				 */
+				tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+										 &tupnull);
+				result = _bt_compare_array_skey(&so->orderProcs[ikey],
+												tupdatum, tupnull,
+												key->sk_argument, key);
+				if (result == 0)
+					continue;	/* safe, = key satisfied by every tuple */
+			}
+			break;				/* pstate.ikey to be set to scalar key's ikey */
+		}
+
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == ikey);
+		if (array->num_elems != -1)
+		{
+			/*
+			 * Found a SAOP array = key.
+			 *
+			 * Handle this just like we handle scalar = keys.
+			 */
+			if (key->sk_attno < firstchangingattnum)
+			{
+				/*
+				 * Only one distinct value on the page for this key's column.
+				 * Must make sure that SAOP array is actually satisfied by the
+				 * value that is stored within every tuple on the page.
+				 */
+				tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+										 &tupnull);
+				_bt_binsrch_array_skey(&so->orderProcs[ikey], false,
+									   NoMovementScanDirection,
+									   tupdatum, tupnull, array, key, &result);
+				if (result == 0)
+					continue;	/* safe, SAOP = key satisfied by every tuple */
+			}
+			break;				/* pstate.ikey to be set to SAOP array's ikey */
+		}
+
+		/*
+		 * Found a skip array = key.
+		 *
+		 * As with other = keys, moving past this skip array key is safe when
+		 * every tuple on the page is guaranteed to satisfy the array's key.
+		 * But we need a slightly different approach, since skip arrays make
+		 * it easy to assess whether all the values on the page fall within
+		 * the skip array's entire range.
+		 */
+		if (array->null_elem)
+		{
+			/* Safe, non-range skip array "satisfied" by every tuple on page */
+			continue;
+		}
+		else if (key->sk_attno > firstchangingattnum)
+		{
+			/*
+			 * We cannot assess whether this range skip array will definitely
+			 * be satisfied by every tuple on the page, since its attribute is
+			 * preceded by another attribute that is not certain to contain
+			 * the same prefix of value(s) within every tuple from pstate.page
+			 */
+			break;				/* pstate.ikey to be set to range array's ikey */
+		}
+
+		/*
+		 * Found a range skip array = key.
+		 *
+		 * It's definitely safe for _bt_checkkeys to avoid assessing this
+		 * range skip array when the page's first and last non-pivot tuples
+		 * both satisfy the range skip array (since the same must also be true
+		 * of all the tuples in between these two).
+		 */
+		tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   tupdatum, tupnull, array, key, &result);
+		if (result != 0)
+			break;				/* pstate.ikey to be set to range array's ikey */
+
+		tupdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   tupdatum, tupnull, array, key, &result);
+		if (result != 0)
+			break;				/* pstate.ikey to be set to range array's ikey */
+
+		/* Safe, range skip array satisfied by every tuple on page */
+	}
+
+	/*
+	 * If pstate.ikey remains 0, _bt_advance_array_keys will still be able to
+	 * apply its precheck optimization when dealing with "nonrequired" array
+	 * keys.  That's reason enough on its own to set forcenonrequired=true.
+	 */
+	pstate->forcenonrequired = true;	/* do this unconditionally */
+	pstate->ikey = ikey;
+
+	/*
+	 * Set the element for range skip arrays whose ikey is >= pstate.ikey to
+	 * whatever the first array element is in the scan's current direction.
+	 * This allows range skip arrays that will never be satisfied by any tuple
+	 * on the page to avoid extra sk_argument comparisons -- _bt_check_compare
+	 * won't use the key's sk_argument when the key is marked MINVAL/MAXVAL
+	 * (note that MINVAL/MAXVAL won't be unset until an exact match is found,
+	 * which might not happen for any tuple on the page).
+	 *
+	 * Set the element for non-range skip arrays whose ikey is >= pstate.ikey
+	 * to NULL (regardless of whether NULLs are stored first or last), too.
+	 * This allows non-range skip arrays (recognized by _bt_check_compare as
+	 * "non-required" skip arrays with ISNULL set) to avoid needlessly calling
+	 * _bt_advance_array_keys (we know that any non-range skip array must be
+	 * satisfied by every possible indexable value, so this is always safe).
+	 */
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		key = &so->keyData[array->scan_key];
+
+		Assert(key->sk_flags & SK_SEARCHARRAY);
+
+		if (array->num_elems != -1)
+			continue;
+		if (array->scan_key < pstate->ikey)
+			continue;
+
+		Assert(key->sk_flags & SK_BT_SKIP);
+
+		if (!array->null_elem)
+			_bt_array_set_low_or_high(rel, key, array,
+									  ScanDirectionIsForward(dir));
+		else
+			_bt_skiparray_set_isnull(rel, key, array);
+	}
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
@@ -2439,17 +2696,25 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Though we advance non-required array keys on our own, that shouldn't have
  * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
+ * have no fixed relationship with the scan's progress.
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass forcenonrequired=true to instruct us to treat all keys as nonrequried.
+ * This is used to make it safe to temporarily stop properly maintaining the
+ * scan's required arrays.  Callers can determine which prefix of keys must
+ * satisfy every possible prefix of index attribute values on the page, and
+ * then pass us an initial *ikey for the first key that might be unsatisified.
+ * We won't be maintaining any arrays before that initial *ikey, so there is
+ * no point in trying to do so for any later arrays.  (Callers that do this
+ * must be careful to reset the array keys when they finish reading the page.)
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool forcenonrequired,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -2466,10 +2731,13 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when we're forced to treat all scan keys as nonrequired)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (forcenonrequired)
+			Assert(!prechecked);
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
@@ -2517,6 +2785,19 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		{
 			Assert(key->sk_flags & SK_SEARCHARRAY);
 			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir || forcenonrequired);
+
+			/*
+			 * Cannot fall back on _bt_tuple_before_array_skeys when we're
+			 * treating the scan's keys as nonrequired, though.  Just handle
+			 * this like any other non-required equality-type array key.
+			 */
+			if (forcenonrequired)
+			{
+				Assert(!(key->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+				return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
+											  tupdesc, *ikey, false);
+			}
 
 			*continuescan = false;
 			return false;
@@ -2526,7 +2807,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
-									 continuescan))
+									 forcenonrequired, continuescan))
 				continue;
 			return false;
 		}
@@ -2558,9 +2839,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			 */
 			if (requiredSameDir)
 				*continuescan = false;
+			else if (unlikely(key->sk_flags & SK_BT_SKIP))
+			{
+				/*
+				 * If we're treating scan keys as nonrequired, and encounter a
+				 * skip array scan key whose current element is NULL, then it
+				 * must be a non-range skip array.  It must be satisfied, so
+				 * there's no need to call _bt_advance_array_keys to check.
+				 */
+				Assert(forcenonrequired && *ikey > 0);
+				continue;
+			}
 
 			/*
-			 * In any case, this indextuple doesn't match the qual.
+			 * This indextuple doesn't match the qual.
 			 */
 			return false;
 		}
@@ -2581,7 +2873,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -2599,7 +2891,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -2668,7 +2960,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
  */
 static bool
 _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
-					 TupleDesc tupdesc, ScanDirection dir, bool *continuescan)
+					 TupleDesc tupdesc, ScanDirection dir,
+					 bool forcenonrequired, bool *continuescan)
 {
 	ScanKey		subkey = (ScanKey) DatumGetPointer(skey->sk_argument);
 	int32		cmpresult = 0;
@@ -2708,7 +3001,11 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 
 		if (isNull)
 		{
-			if (subkey->sk_flags & SK_BT_NULLS_FIRST)
+			if (forcenonrequired)
+			{
+				/* treating scan key as non-required */
+			}
+			else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -2762,8 +3059,12 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			 */
 			Assert(subkey != (ScanKey) DatumGetPointer(skey->sk_argument));
 			subkey--;
-			if ((subkey->sk_flags & SK_BT_REQFWD) &&
-				ScanDirectionIsForward(dir))
+			if (forcenonrequired)
+			{
+				/* treating scan key as non-required */
+			}
+			else if ((subkey->sk_flags & SK_BT_REQFWD) &&
+					 ScanDirectionIsForward(dir))
 				*continuescan = false;
 			else if ((subkey->sk_flags & SK_BT_REQBKWD) &&
 					 ScanDirectionIsBackward(dir))
@@ -2815,7 +3116,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			break;
 	}
 
-	if (!result)
+	if (!result && !forcenonrequired)
 	{
 		/*
 		 * Tuple fails this qual.  If it's a required qual for the current
@@ -2859,6 +3160,8 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	Assert(!pstate->forcenonrequired);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
-- 
2.47.2

v28-0005-DEBUG-Add-skip-scan-disable-GUCs.patchapplication/octet-stream; name=v28-0005-DEBUG-Add-skip-scan-disable-GUCs.patchDownload
From 0df72d07f8644f21e3a5fc5843c7f315a08b814d Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 18 Jan 2025 10:54:44 -0500
Subject: [PATCH v28 5/5] DEBUG: Add skip scan disable GUCs.

---
 src/include/access/nbtree.h                   |  4 +++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 31 +++++++++++++++++++
 src/backend/utils/misc/guc_tables.c           | 23 ++++++++++++++
 3 files changed, 58 insertions(+)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 60a0c0e65..0d605f59a 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1184,6 +1184,10 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index c48c4f6c1..131e227e5 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -21,6 +21,27 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
 typedef struct BTScanKeyPreproc
 {
 	ScanKey		inkey;
@@ -1644,6 +1665,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
 			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
 
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].sksup = NULL;
+
 			/*
 			 * We'll need a 3-way ORDER proc.  Set that up now.
 			 */
@@ -2148,6 +2173,12 @@ _bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
 		if (attno_has_rowcompare)
 			break;
 
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
 		/*
 		 * Now consider next attno_inkey (or keep going if this is an
 		 * additional scan key against the same attribute)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index ad25cbb39..7e4fa5c31 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1784,6 +1785,17 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3661,6 +3673,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
-- 
2.47.2

#69Alena Rybakina
a.rybakina@postgrespro.ru
In reply to: Peter Geoghegan (#68)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 11.03.2025 18:52, Peter Geoghegan wrote:

On Sat, Mar 8, 2025 at 11:43 AM Peter Geoghegan <pg@bowt.ie> wrote:

I plan on committing this one soon. It's obviously pretty pointless to
make the BTMaxItemSize operate off of a page header, and not requiring
it is more flexible.

Committed. And committed a revised version of "Show index search count
in EXPLAIN ANALYZE" that addresses the issues with non-parallel-aware
index scan executor nodes that run from a parallel worker.

Hi, reviewing the code I noticed that you removed the
parallel_aware check for DSM initialization for BitmapIndexScan,
IndexScan, IndexOnlyScan,
but you didn't do the same in the ExecParallelReInitializeDSM function
and I can't figure out why to be honest. I think it might be wrong or
I'm missing something.

As I see, it might be necessary if the parallel executor needs to
reinitialize the shared memory state before launching a fresh batches of
workers (it is based on
the comment of the ExecParallelReinitialize function), and when it
happens all child nodes reset their state (see the comment next to the
call to the ExecParallelReInitializeDSM
function).

So, what do you think? Should the check be removed in the
ExecParallelReInitializeDSM function too?

--
Regards,
Alena Rybakina
Postgres Professional

In reply to: Alena Rybakina (#69)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Mar 11, 2025 at 6:24 PM Alena Rybakina
<a.rybakina@postgrespro.ru> wrote:

Hi, reviewing the code I noticed that you removed the
parallel_aware check for DSM initialization for BitmapIndexScan,
IndexScan, IndexOnlyScan,
but you didn't do the same in the ExecParallelReInitializeDSM function
and I can't figure out why to be honest. I think it might be wrong or
I'm missing something.

I didn't exactly remove the check -- not completely. You could say
that I *moved* the check, from the caller (i.e. from functions in
execParallel.c such as ExecParallelInitializeDSM) to the callee (i.e.
into individual executor node functions such as
ExecIndexScanInitializeDSM). I did it that way because it's more
flexible.

We need this flexibility because we need to allocate DSM for
instrumentation state when EXPLAIN ANALYZE runs a parallel query with
an index scan node -- even when the scan node runs inside a parallel
worker, but is non-parallel-aware (parallel oblivious). Obviously, we
might also need to allocate space for a shared index scan descriptor
(including index AM opaque state), in the same way as before
(or we might need to do both).

As I see, it might be necessary if the parallel executor needs to
reinitialize the shared memory state before launching a fresh batches of
workers (it is based on
the comment of the ExecParallelReinitialize function), and when it
happens all child nodes reset their state (see the comment next to the
call to the ExecParallelReInitializeDSM
function).

I did not move/remove the parallel_aware check in
ExecParallelReInitializeDSM because it doesn't have the same
requirements -- we *don't* need that flexibility there, because it
isn't necessary (or correct) to reinitialize anything when the only
thing that's in DSM is instrumentation state. (Though I did add an
assertion about parallel_aware-ness to functions like
ExecIndexScanReInitializeDSM, which I thought might make this a little
clearer to people reading files like nodeIndexScan.c, and wondering
why ExecIndexScanReInitializeDSM doesn't specifically test
parallel_aware.)

Obviously, these node types don't have their state reset (quoting
ExecParallelReInitializeDSM switch statement here):

case T_BitmapIndexScanState:
case T_HashState:
case T_SortState:
case T_IncrementalSortState:
case T_MemoizeState:
/* these nodes have DSM state, but no reinitialization is
required */
break;

I added T_BitmapIndexScanState to the top of this list -- the rest are
from before today's commit. I did this since (like the other nodes
shown) BitmapIndexScan's use of DSM is limited to instrumentation
state -- which we never want to reset (there is no such thing as a
parallel bitmap index scan, though bitmap index scans can run in
parallel workers, and still need the instrumentation to work with
EXPLAIN ANALYZE).

We still need to call functions like ExecIndexOnlyScanReInitializeDSM
from here/ExecParallelReInitializeDSM, of course, but that won't reset
the new instrumentation state (because my patch didn't touch it at
all, except for adding that assertion I already mentioned in passing).

We actually specifically rely on *not* resetting the shared memory
state to get correct behavior in cases like this one:

/messages/by-id/CAAKRu_YjBPfGp85ehY1t9NN=R9pB9k=6rztaeVkAm-OeTqUK4g@mail.gmail.com

See comments about this in places like ExecEndBitmapIndexScan (added
by today's commit), or in ExecEndBitmapHeapScan (added by the similar
bitmap heap instrumentation patch discussed on that other thread,
which became commit 5a1e6df3).

--
Peter Geoghegan

#71Alena Rybakina
a.rybakina@postgrespro.ru
In reply to: Peter Geoghegan (#70)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 12.03.2025 01:59, Peter Geoghegan wrote:

On Tue, Mar 11, 2025 at 6:24 PM Alena Rybakina
<a.rybakina@postgrespro.ru> wrote:

Hi, reviewing the code I noticed that you removed the
parallel_aware check for DSM initialization for BitmapIndexScan,
IndexScan, IndexOnlyScan,
but you didn't do the same in the ExecParallelReInitializeDSM function
and I can't figure out why to be honest. I think it might be wrong or
I'm missing something.

I didn't exactly remove the check -- not completely. You could say
that I *moved* the check, from the caller (i.e. from functions in
execParallel.c such as ExecParallelInitializeDSM) to the callee (i.e.
into individual executor node functions such as
ExecIndexScanInitializeDSM). I did it that way because it's more
flexible.

We need this flexibility because we need to allocate DSM for
instrumentation state when EXPLAIN ANALYZE runs a parallel query with
an index scan node -- even when the scan node runs inside a parallel
worker, but is non-parallel-aware (parallel oblivious). Obviously, we
might also need to allocate space for a shared index scan descriptor
(including index AM opaque state), in the same way as before
(or we might need to do both).

I see and agree with this changes now.

As I see, it might be necessary if the parallel executor needs to
reinitialize the shared memory state before launching a fresh batches of
workers (it is based on
the comment of the ExecParallelReinitialize function), and when it
happens all child nodes reset their state (see the comment next to the
call to the ExecParallelReInitializeDSM
function).

I did not move/remove the parallel_aware check in
ExecParallelReInitializeDSM because it doesn't have the same
requirements -- we *don't* need that flexibility there, because it
isn't necessary (or correct) to reinitialize anything when the only
thing that's in DSM is instrumentation state. (Though I did add an
assertion about parallel_aware-ness to functions like
ExecIndexScanReInitializeDSM, which I thought might make this a little
clearer to people reading files like nodeIndexScan.c, and wondering
why ExecIndexScanReInitializeDSM doesn't specifically test
parallel_aware.)

Obviously, these node types don't have their state reset (quoting
ExecParallelReInitializeDSM switch statement here):

case T_BitmapIndexScanState:
case T_HashState:
case T_SortState:
case T_IncrementalSortState:
case T_MemoizeState:
/* these nodes have DSM state, but no reinitialization is
required */
break;

I added T_BitmapIndexScanState to the top of this list -- the rest are
from before today's commit. I did this since (like the other nodes
shown) BitmapIndexScan's use of DSM is limited to instrumentation
state -- which we never want to reset (there is no such thing as a
parallel bitmap index scan, though bitmap index scans can run in
parallel workers, and still need the instrumentation to work with
EXPLAIN ANALYZE).

We still need to call functions like ExecIndexOnlyScanReInitializeDSM
from here/ExecParallelReInitializeDSM, of course, but that won't reset
the new instrumentation state (because my patch didn't touch it at
all, except for adding that assertion I already mentioned in passing).

We actually specifically rely on *not* resetting the shared memory
state to get correct behavior in cases like this one:

/messages/by-id/CAAKRu_YjBPfGp85ehY1t9NN=R9pB9k=6rztaeVkAm-OeTqUK4g@mail.gmail.com

See comments about this in places like ExecEndBitmapIndexScan (added
by today's commit), or in ExecEndBitmapHeapScan (added by the similar
bitmap heap instrumentation patch discussed on that other thread,
which became commit 5a1e6df3).

Thank you for the explanation!

Now I see why these changes were made.

After your additional explanations, everything really became clear and I
fully agree with the current code regarding this part.
However I did not see an explanation to the commit regarding this place,
as well as a comment next to the assert and the parallel_aware check and
why BitmapIndexScanState was added in the ExecParallelReInitializeDSM.
In my opinion, there is not enough additional explanation about this in
the form of comments, although I think that it has already been
explained here enough for someone who will look at this code.

--
Regards,
Alena Rybakina
Postgres Professional

In reply to: Alena Rybakina (#71)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Mar 12, 2025 at 4:28 PM Alena Rybakina
<a.rybakina@postgrespro.ru> wrote:

Thank you for the explanation!

Now I see why these changes were made.

After your additional explanations, everything really became clear and I
fully agree with the current code regarding this part.

Cool.

However I did not see an explanation to the commit regarding this place,
as well as a comment next to the assert and the parallel_aware check and
why BitmapIndexScanState was added in the ExecParallelReInitializeDSM.

I added BitmapIndexScanState to the switch statement in
ExecParallelReInitializeDSM because it is in the category of
planstates that never need their shared memory reinitialized -- that's
just how we represent such a plan state there.

I think that this is supposed to serve as a kind of documentation,
since it doesn't really affect how things behave. That is, it wouldn't
actually affect anything if I had forgotten to add
BitmapIndexScanState to the ExecParallelReInitializeDSM switch
statement "case" that represents that it is in this "plan state
category": the switch ends with catch-all "default: break;".

In my opinion, there is not enough additional explanation about this in
the form of comments, although I think that it has already been
explained here enough for someone who will look at this code.

What can be done to improve the situation? For example, would adding a
comment next to the new assertions recently added to
ExecIndexScanReInitializeDSM and ExecIndexOnlyScanReInitializeDSM be
an improvement? And if so, what would the comment say?

--
Peter Geoghegan

#73Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Peter Geoghegan (#68)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, 11 Mar 2025 at 16:53, Peter Geoghegan <pg@bowt.ie> wrote:

On Sat, Mar 8, 2025 at 11:43 AM Peter Geoghegan <pg@bowt.ie> wrote:

I plan on committing this one soon. It's obviously pretty pointless to
make the BTMaxItemSize operate off of a page header, and not requiring
it is more flexible.

Committed. And committed a revised version of "Show index search count
in EXPLAIN ANALYZE" that addresses the issues with non-parallel-aware
index scan executor nodes that run from a parallel worker.

Attached is v28. This is just to keep the patch series applying
cleanly -- no real changes here.

You asked off-list for my review of 0003. I'd already reviewed 0001
before that, so that review also included. I'll see if I can spend
some time on the other patches too, but for 0003 I think I got some
good consistent feedback.

0001:

src/backend/access/nbtree/nbtsearch.c
_bt_readpage

This hasn't changed meaningfully in this patch, but I noticed that
pstate.finaltup is never set for the final page of the scan direction
(i.e. P_RIGHTMOST or P_LEFTMOST for forward or backward,
respectively). If it isn't used more than once after the first element
of non-P_RIGHTMOST/LEFTMOST pages, why is it in pstate? Or, if it is
used more than once, why shouldn't it be used in

Apart from that, 0001 looks good to me.

0003:

_bt_readpage

In forward scan mode, recovery from forcenonrequired happens after the
main loop over all page items. In backward mode, it's in the loop:

+            if (offnum == minoff && pstate.forcenonrequired)
+            {
+                Assert(so->skipScan);

I think there's a comment missing that details _why_ we do this;
probably something like:

/*
* We're about to process the final item on the page.
* Un-set forcenonrequired, so the next _bt_checkkeys will
* evaluate required scankeys and signal an end to this
* primitive scan if we've reached a stopping point.
*/

In line with that, could you explain a bit more about the
pstate.forcenonrequired optimization? I _think_ it's got something to
do with "required" scankeys adding some overhead per scankey, which
can be significant with skipscan evaluations and ignoring the
requiredness can thus save some cycles, but the exact method doesn't
seem to be very well articulated.

_bt_skip_ikeyprefix

I _think_ it's worth special-casing firstchangingattnum=1, as in that
case we know in advance there is no (immediate) common ground between
the index tuples and thus any additional work we do towards parsing
the scankeys would be wasted - except for matching inequality bounds
for firstchangingatt, or matching "open" skip arrays for a prefix of
attributes starting at firstchangingattnum (as per the
array->null_elem case).

I also notice somed some other missed opportunities for optimizing
page accesses:

+ if (key->sk_strategy != BTEqualStrategyNumber)

The code halts optimizing "prefix prechecks" when we notice a
non-equality key. It seems to me that we can do the precheck on shared
prefixes with non-equality keys just the same as with equality keys;
and it'd improve performance in those cases, too.

+        if (!(key->sk_flags & SK_SEARCHARRAY))
+            if (key->sk_attno < firstchangingattnum)
+            {
+                if (result == 0)
+                    continue;    /* safe, = key satisfied by every tuple */
+            }
+            break;                /* pstate.ikey to be set to scalar key's ikey */

This code finds out that no tuple on the page can possibly match the
scankey (idxtup=scalar returns non-0 value) but doesn't (can't) use it
to exit the scan. I think that's a missed opportunity for
optimization; now we have to figure that out for every tuple in the
scan. Same applies to the SAOP -array case (i.e. non-skiparray).

Thank you for working on this.

Kind regards,

Matthias van de Meent
Neon (https://neon.tech)

#74Alena Rybakina
a.rybakina@postgrespro.ru
In reply to: Peter Geoghegan (#72)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 12.03.2025 23:50, Peter Geoghegan wrote:

On Wed, Mar 12, 2025 at 4:28 PM Alena Rybakina
<a.rybakina@postgrespro.ru> wrote:

Thank you for the explanation!

Now I see why these changes were made.

After your additional explanations, everything really became clear and I
fully agree with the current code regarding this part.

Cool.

However I did not see an explanation to the commit regarding this place,
as well as a comment next to the assert and the parallel_aware check and
why BitmapIndexScanState was added in the ExecParallelReInitializeDSM.

I added BitmapIndexScanState to the switch statement in
ExecParallelReInitializeDSM because it is in the category of
planstates that never need their shared memory reinitialized -- that's
just how we represent such a plan state there.

I think that this is supposed to serve as a kind of documentation,
since it doesn't really affect how things behave. That is, it wouldn't
actually affect anything if I had forgotten to add
BitmapIndexScanState to the ExecParallelReInitializeDSM switch
statement "case" that represents that it is in this "plan state
category": the switch ends with catch-all "default: break;".

Agree.

In my opinion, there is not enough additional explanation about this in
the form of comments, although I think that it has already been
explained here enough for someone who will look at this code.

What can be done to improve the situation? For example, would adding a
comment next to the new assertions recently added to
ExecIndexScanReInitializeDSM and ExecIndexOnlyScanReInitializeDSM be
an improvement? And if so, what would the comment say?

After reviewing the logic again, I realized that I was confused
precisely in the reinitialization of memory for IndexScanState and
IndexOnlyScanState.

As far as I can see, either assert is not needed here, the functions
ExecIndexScanReInitializeDSM and ExecIndexScanReInitializeDSM can be
called only if parallel_aware is positive, or it makes sense that
reinitialization is needed only if parallel_aware is positive, then the
condition noted above is not needed. According to your letter (0), the
check should be removed there too, but I got confused in the comment. We
do not need to reinitialize memory because DSM is instrumentation state
only, but it turns out that we are reinitializing the memory, so we
don't do it at all?

I attached a diff file to the letter with the comment.

[0]: /messages/by-id/CAH2-WzkMpFsE_hM9-5tecF22jVJSGtKMFMsYqMa-uo73MOxsWw@mail.gmail.com
/messages/by-id/CAH2-WzkMpFsE_hM9-5tecF22jVJSGtKMFMsYqMa-uo73MOxsWw@mail.gmail.com

--
Regards,
Alena Rybakina
Postgres Professional

Attachments:

for_reinitialize_memory.diff.no-cfbottext/plain; charset=UTF-8; name=for_reinitialize_memory.diff.no-cfbotDownload
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index e9337a97d17..c8f3300ec00 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -977,14 +977,14 @@ ExecParallelReInitializeDSM(PlanState *planstate,
 				ExecSeqScanReInitializeDSM((SeqScanState *) planstate,
 										   pcxt);
 			break;
+
+		/* The parallel_aware is always true for IndexScan, IndexOnlyScan */
 		case T_IndexScanState:
-			if (planstate->plan->parallel_aware)
-				ExecIndexScanReInitializeDSM((IndexScanState *) planstate,
+			ExecIndexScanReInitializeDSM((IndexScanState *) planstate,
 											 pcxt);
 			break;
 		case T_IndexOnlyScanState:
-			if (planstate->plan->parallel_aware)
-				ExecIndexOnlyScanReInitializeDSM((IndexOnlyScanState *) planstate,
+			ExecIndexOnlyScanReInitializeDSM((IndexOnlyScanState *) planstate,
 												 pcxt);
 			break;
 		case T_ForeignScanState:
#75Alena Rybakina
a.rybakina@postgrespro.ru
In reply to: Alena Rybakina (#74)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 18.03.2025 13:54, Alena Rybakina wrote:

On 12.03.2025 23:50, Peter Geoghegan wrote:

On Wed, Mar 12, 2025 at 4:28 PM Alena Rybakina
<a.rybakina@postgrespro.ru> wrote:

Thank you for the explanation!

Now I see why these changes were made.

After your additional explanations, everything really became clear and I
fully agree with the current code regarding this part.

Cool.

However I did not see an explanation to the commit regarding this place,
as well as a comment next to the assert and the parallel_aware check and
why BitmapIndexScanState was added in the ExecParallelReInitializeDSM.

I added BitmapIndexScanState to the switch statement in
ExecParallelReInitializeDSM because it is in the category of
planstates that never need their shared memory reinitialized -- that's
just how we represent such a plan state there.

I think that this is supposed to serve as a kind of documentation,
since it doesn't really affect how things behave. That is, it wouldn't
actually affect anything if I had forgotten to add
BitmapIndexScanState to the ExecParallelReInitializeDSM switch
statement "case" that represents that it is in this "plan state
category": the switch ends with catch-all "default: break;".

Agree.

In my opinion, there is not enough additional explanation about this in
the form of comments, although I think that it has already been
explained here enough for someone who will look at this code.

What can be done to improve the situation? For example, would adding a
comment next to the new assertions recently added to
ExecIndexScanReInitializeDSM and ExecIndexOnlyScanReInitializeDSM be
an improvement? And if so, what would the comment say?

After reviewing the logic again, I realized that I was confused
precisely in the reinitialization of memory for IndexScanState and
IndexOnlyScanState.

As far as I can see, either assert is not needed here, the functions
ExecIndexScanReInitializeDSM and ExecIndexScanReInitializeDSM can be
called only if parallel_aware is positive, or it makes sense that
reinitialization is needed only if parallel_aware is positive, then
the condition noted above is not needed. According to your letter (0),
the check should be removed there too, but I got confused in the
comment. We do not need to reinitialize memory because DSM is
instrumentation state only, but it turns out that we are
reinitializing the memory, so we don't do it at all?

I attached a diff file to the letter with the comment.

[0]
/messages/by-id/CAH2-WzkMpFsE_hM9-5tecF22jVJSGtKMFMsYqMa-uo73MOxsWw@mail.gmail.com

Sorry, I figured it out. The Assert was added to avoid misuse of the
function to reinitialize memory and to ensure that it happens when
parallel_aware is positive.

--
Regards,
Alena Rybakina
Postgres Professional

In reply to: Alena Rybakina (#75)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Mar 18, 2025 at 9:37 AM Alena Rybakina
<a.rybakina@postgrespro.ru> wrote:

Sorry, I figured it out. The Assert was added to avoid misuse of the function to reinitialize memory and to ensure that it happens when parallel_aware is positive.

Yeah. The assertion is supposed to suggest "don't worry, I didn't
forget to test parallel_aware in this function
[ExecIndexScanReInitializeDSM or ExecIndexScanReInitializeDSM], it's
just that I expect to not be called unless parallel_aware is true".

An alternative approach would have been to explicitly handle
parallel_aware=false in ExecIndexScanReInitializeDSM and
ExecIndexScanReInitializeDSM: they could just return false. That would
make ExecIndexScanReInitializeDSM and ExecIndexScanReInitializeDSM
more consistent with other nearby functions (in nodeIndexScan.c and
nodeIndexOnlyScan.c), at the cost of making the calling code (in
execParallel.c) less consistent. I really don't think that that
alternative is any better to what I actually did (I actually
considered doing things this way myself at one point), though it also
doesn't seem worse. So I'm inclined to do nothing about it now.

--
Peter Geoghegan

#77Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Matthias van de Meent (#73)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Mon, 17 Mar 2025 at 23:51, Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:

On Tue, 11 Mar 2025 at 16:53, Peter Geoghegan <pg@bowt.ie> wrote:

On Sat, Mar 8, 2025 at 11:43 AM Peter Geoghegan <pg@bowt.ie> wrote:

I plan on committing this one soon. It's obviously pretty pointless to
make the BTMaxItemSize operate off of a page header, and not requiring
it is more flexible.

Committed. And committed a revised version of "Show index search count
in EXPLAIN ANALYZE" that addresses the issues with non-parallel-aware
index scan executor nodes that run from a parallel worker.

Attached is v28. This is just to keep the patch series applying
cleanly -- no real changes here.

You asked off-list for my review of 0003. I'd already reviewed 0001
before that, so that review also included. I'll see if I can spend
some time on the other patches too

My comments on 0004:

_bt_skiparray_strat_decrement
_bt_skiparray_strat_increment

In both functions the generated value isn't used when the in/decrement
overflows (and thus invalidates the qual), or when the opclass somehow
doesn't have a <= or >= operator, respectively.
For byval types that's not much of an issue, but for by-ref types
(such as uuid, or bigint on 32-bit systems) that's not great, as btree
explicitly allows no leaks for the in/decrement functions, and now we
use those functions and leak the values.

Additionally, the code is essentially duplicated between the
functions, with as only differences which sksup function to call;
which opstrategies to check, and where to retrieve/put the value. It's
only 2 instances total, but if you figure out how to make a nice
single function from the two that'd be appreciated, as it reduces
duplication and chances for divergence.

Kind regards,

Matthias van de Meent
Neon (https://neon.tech)

In reply to: Matthias van de Meent (#73)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Mon, Mar 17, 2025 at 6:51 PM Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:

This hasn't changed meaningfully in this patch, but I noticed that
pstate.finaltup is never set for the final page of the scan direction
(i.e. P_RIGHTMOST or P_LEFTMOST for forward or backward,
respectively). If it isn't used more than once after the first element
of non-P_RIGHTMOST/LEFTMOST pages, why is it in pstate? Or, if it is
used more than once, why shouldn't it be used in

We don't set pstate.finaltup on either the leftmost or rightmost page
(nothing new, this is all from the Postgres 17 SAOP patch) because we
cannot possibly need to start another primitive index scan from that
point. We only use pstate.finaltup to determine if we should start a
new primitive index scan, every time the scan's array keys advance
(except during "sktrig_required=false" array advancement, and barring
the case where we don't have to check pstate.finaltup because we see
that the new set of array keys are already an exact match for the
tuple passed to _bt_advance_array_keys). In short, pstate.finaltup is
used during a significant fraction of all calls to
_bt_advance_array_keys, and there is no useful limit on the number of
times that pstate.finaltup can be used per _bt_readpage.

Although you didn't say it in your email (you said it to me on our
most recent call), I believe that you're asking about pstate.finaltup
for reasons that are more related to 0003-* than to 0001-*. As I
recall, you asked me about why it was that the 0003-* patch avoids
_bt_readpage's call to _bt_skip_ikeyprefix on the leftmost or
rightmost leaf page (i.e. a page that lacks a pstate.finaltup). You
were skeptical about that -- understandably so.

To recap, 0003-* avoids calling _bt_skip_ikeyprefix on the rightmost
(and leftmost) page because it reasons that activating that
optimization is only okay when we know that we'll be able to "recover"
afterwards, during the finaltup _bt_checkkeys call. By "recover", I
mean restore the invariant for required array keys: that they track
our progress through the key space. It felt safer and easier for
0003-* to just not call _bt_skip_ikeyprefix on a page that has no
finaltup -- that way we won't have anything that we need to recover
from, when we lack the pstate.finaltup that we generally expect to use
for this purpose. This approach allowed me to add the following
assertions a little bit later on, right at the end of _bt_readpage
(quoting 0003-* here):

@@ -1993,6 +2031,16 @@ _bt_readpage(IndexScanDesc scan, ScanDirection
dir, OffsetNumber offnum,
so->currPos.itemIndex = MaxTIDsPerBTreePage - 1;
}

+   /*
+    * As far as our caller is concerned, the scan's arrays always track its
+    * progress through the index's key space.
+    *
+    * If _bt_skip_ikeyprefix told us to temporarily treat all scan keys as
+    * nonrequired (during a skip scan), then we must recover afterwards by
+    * advancing our arrays using finaltup (with !pstate.forcenonrequired).
+    */
+   Assert(pstate.ikey == 0 && !pstate.forcenonrequired);
+
    return (so->currPos.firstItem <= so->currPos.lastItem);
 }

I've actually come around to your point of view on this (or what I
thought was your PoV from our call). That is, I now *think* that it
would be better if the code added by 0003-* called
_bt_skip_ikeyprefix, without regard for whether or not we'll have a
finaltup _bt_checkkeys call to "recover" (i.e. whether we're on the
leftmost or rightmost page shouldn't matter).

My change in perspective on this question is related to another change
of perspective, on the question of whether we actually need to call
_bt_start_array_keys as part of "recovering/restoring the array
invariant", just ahead of the finaltup _bt_checkkeys call. As you
know, 0003-* calls _bt_start_array_keys in this way, but that now
seems like overkill. It can have undesirable side-effects when the
array keys spuriously appear to advance, when in fact they were
restarted via the _bt_start_array_keys call, only to have their
original values restored via the finaltup call that immediately
follows.

Here's what I mean about side-effects:

We shouldn't allow _bt_advance_array_keys' primitive index scan
scheduling logic to falsely believe that the array keys have advanced,
when they didn't really advance in any practical sense. The commit
message of 0003-* promises that its optimization cannot influence
primitive index scan scheduling at all, which seems like a good
general principle for us to follow. But I recently discovered that
that promise was subtly broken, which I tied to the way that 0003-*
calls _bt_start_array_keys (this didn't produce wrong answers, and
didn't even really hurt performance, but it seems wonky and hard to
test). So I now think that I need to expect more from
_bt_skip_ikeyprefix and its pstate.forcenonrequired optimization, so
that my promise about primscan scheduling is honored.

Tying it back to your concern, once I do that (once I stop calling
_bt_start_array_keys in 0003-* to "hard reset" the arrays), I can also
stop caring about finaltup being set on the rightmost page, at the
point where we decide if _bt_skip_ikeyprefix should be called.

Here's how I think that this will be safe:

Obviously, we can't expect _bt_skip_ikeyprefix/pstate.forcenonrequired
mode to maintain the scan's required arrays in the usual way -- the
whole idea in 0003-* is to stop properly maintaining the arrays, until
right at the end of the _bt_readpage call, so as to save cycles.
However, it should be possible to teach _bt_skip_ikeyprefix to not
leave the array keys in an irredeemably bad state -- even when no
finaltup call is possible during the same _bt_readpage. And, even when
the scan direction changes at an inconvenient time. The next revision
(v29) is likely to strengthen the guarantees that _bt_skip_ikeyprefix
makes about the state that it'll leave the scan's array keys in, so
that its _bt_readpage caller can be laxer about "fully undoing" its
call to _bt_skip_ikeyprefix. The invariants we need to restore only
really apply when the scan needs to continue in the same scan
direction, at least one more page.

In short, as long as the array keys can never "go backwards" (relative
to the current scan direction), then we'll be able to recover during
the next conventional call to _bt_checkkeys (meaning the next
!pstate.forcenonrequired call) -- even if that next call should happen
on some other page (in practice it is very unlikely that it will, but
we can still be prepared for that). While it's true that
_bt_advance_array_keys (with sktrig_required=true) always promises to
advance the array keys to the maximum possible extent that it can know
to be safe, based on the caller's tuple alone, that in itself doesn't
obligate _bt_readpage to make sure that _bt_advance_array_keys will be
called when the top-level scan is over. We have never expected
_bt_advance_array_keys to *reliably* reach the final set of array keys
at the end of the scan (e.g., this won't happen when the index is
completely empty, since we'll never call _bt_readpage in the first
place).

In forward scan mode, recovery from forcenonrequired happens after the
main loop over all page items. In backward mode, it's in the loop:

+            if (offnum == minoff && pstate.forcenonrequired)
+            {
+                Assert(so->skipScan);

I think there's a comment missing that details _why_ we do this;
probably something like:

/*
* We're about to process the final item on the page.
* Un-set forcenonrequired, so the next _bt_checkkeys will
* evaluate required scankeys and signal an end to this
* primitive scan if we've reached a stopping point.
*/

I think that the right place to talk about this is above
_bt_skip_ikeyprefix itself.

In line with that, could you explain a bit more about the
pstate.forcenonrequired optimization? I _think_ it's got something to
do with "required" scankeys adding some overhead per scankey, which
can be significant with skipscan evaluations and ignoring the
requiredness can thus save some cycles, but the exact method doesn't
seem to be very well articulated.

The main benefit of the _bt_skip_ikeyprefix optimization is that it
allows us to skip a pstate.ikey prefix of scan keys in many important
cases. But that is not compatible with certain existing
_bt_advance_array_keys "sktrig_required=true" optimizations.

Most notably, we cannot assume that the array keys perfectly track our
progress through the index's key space when calling
_bt_advance_array_keys with "sktrig_required=false". In particular, it
would be wrong to allow the SAOP array binary search
cur_elem_trig=true optimization (see _bt_binsrch_array_skey) to be
used. We also don't want to attempt to end the primscan during
"sktrig_required=false" calls to _bt_advance_array_keys (nothing about
that is new here, it's just that _bt_skip_ikeyprefix now temporarily
forces the scan to behave this way, dynamically, rather than it being
static behavior that is fixed for the whole scan).

The underlying problem is that "sktrig_required=false" array
advancement cannot precisely reason about the relationship between the
scan's progress and the current required array key positions. For
example, with a query "WHERE a BETWEEN 0 AND 100 AND b in (42, 44)",
on a page whose "a" attribute values all satisfy the range qual on "a"
(i.e. with pstate.ikey = 1), our "a" skip array won't advance at all
(if we didn't use the _bt_skip_ikeyprefix optimization then we'd only
ever do "sktrig_required=false" advancement, and the skip array might
advance several times within a page, but we're ignoring "a" here). We
cannot reuse work across "b" SAOP binary searches, because in general
we're not paying attention to "a" at all -- and so we won't even try
to roll over "b" when the value of "a" advances (we're just looking at
"b", never "a").

_bt_skip_ikeyprefix

I _think_ it's worth special-casing firstchangingattnum=1, as in that
case we know in advance there is no (immediate) common ground between
the index tuples and thus any additional work we do towards parsing
the scankeys would be wasted - except for matching inequality bounds
for firstchangingatt, or matching "open" skip arrays for a prefix of
attributes starting at firstchangingattnum (as per the
array->null_elem case).

Not sure what you mean by "special-casing firstchangingattnum=1"? What
"additional work we do towards parsing the scankeys" are you concerned
about?

It's fairly common for firstchangingattnum=1, even when the
_bt_skip_ikeyprefix optimization is working well. For example, that's
what'd happen with the example query I just gave (a query "WHERE a
BETWEEN 0 AND 100 AND b in (42, 44)" can skip "a" by setting
pstate.ikey=1, provided all of the "a" attribute values on the page
are within the range of the skip array).

I also notice somed some other missed opportunities for optimizing
page accesses:

+ if (key->sk_strategy != BTEqualStrategyNumber)

The code halts optimizing "prefix prechecks" when we notice a
non-equality key. It seems to me that we can do the precheck on shared
prefixes with non-equality keys just the same as with equality keys;
and it'd improve performance in those cases, too.

Yeah, I was thinking of doing this already (though not for RowCompare
inequalities, which would be hard to evaluate from here). It makes
sense because it's exactly the same case as the range skip array case,
really -- why not just do it the same way?

+        if (!(key->sk_flags & SK_SEARCHARRAY))
+            if (key->sk_attno < firstchangingattnum)
+            {
+                if (result == 0)
+                    continue;    /* safe, = key satisfied by every tuple */
+            }
+            break;                /* pstate.ikey to be set to scalar key's ikey */

This code finds out that no tuple on the page can possibly match the
scankey (idxtup=scalar returns non-0 value) but doesn't (can't) use it
to exit the scan. I think that's a missed opportunity for
optimization; now we have to figure that out for every tuple in the
scan. Same applies to the SAOP -array case (i.e. non-skiparray).

Maybe, but it's not much of a missed opportunity. It doesn't guarantee
that the scan can end in the case of a SAOP (the very next leaf page
could satisfy the same scan key, given a SAOP array with "gaps" in the
elements). So it can only really end the scan with a scalar = key --
though never when it is preceded by a skip array (doesn't matter if
the skip array is definitely satisfied/has only one distinct attribute
value on the page). Is this idea related to your previous idea
involving "special-casing firstchangingattnum=1"?

If I was going to do something like this, I think that it'd work by
backing out of applying the optimization entirely. Right now, 0003-*
applies the optimization whenever _bt_readpage decides to call
_bt_skip_ikeyprefix, regardless of the details after that (it would be
easy to teach _bt_skip_ikeyprefix to decide against applying its
optimization entirely, but testing seems to show that it always makes
sense to go ahead when _bt_skip_ikeyprefix is called, regardless of
what _bt_skip_ikeyprefix sees on the page).

Thank you for working on this.

Thanks for the review!

--
Peter Geoghegan

In reply to: Matthias van de Meent (#77)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Mar 18, 2025 at 1:01 PM Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:

My comments on 0004:

_bt_skiparray_strat_decrement
_bt_skiparray_strat_increment

In both functions the generated value isn't used when the in/decrement
overflows (and thus invalidates the qual), or when the opclass somehow
doesn't have a <= or >= operator, respectively.
For byval types that's not much of an issue, but for by-ref types
(such as uuid, or bigint on 32-bit systems) that's not great, as btree
explicitly allows no leaks for the in/decrement functions, and now we
use those functions and leak the values.

We don't leak any memory here. We temporarily switch over to using
so->arrayContext, for the duration of these calls. And so the memory
will be freed on rescan -- there's a MemoryContextReset call at the
top of _bt_preprocess_array_keys to take care of this, which is hit on
rescan.

Additionally, the code is essentially duplicated between the
functions, with as only differences which sksup function to call;
which opstrategies to check, and where to retrieve/put the value. It's
only 2 instances total, but if you figure out how to make a nice
single function from the two that'd be appreciated, as it reduces
duplication and chances for divergence.

I'll see if I can do something like that for the next revision, but I
think that it might be more awkward than it's worth.

The duplication isn't quite duplication, so much as it's two functions
that are mirror images of each other. The details/direction of things
is flipped in a number of places. The strategy number differs, even
though the same function is called.

--
Peter Geoghegan

In reply to: Peter Geoghegan (#78)
5 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Mar 18, 2025 at 3:15 PM Peter Geoghegan <pg@bowt.ie> wrote:

I've actually come around to your point of view on this (or what I
thought was your PoV from our call). That is, I now *think* that it
would be better if the code added by 0003-* called
_bt_skip_ikeyprefix, without regard for whether or not we'll have a
finaltup _bt_checkkeys call to "recover" (i.e. whether we're on the
leftmost or rightmost page shouldn't matter).

My change in perspective on this question is related to another change
of perspective, on the question of whether we actually need to call
_bt_start_array_keys as part of "recovering/restoring the array
invariant", just ahead of the finaltup _bt_checkkeys call. As you
know, 0003-* calls _bt_start_array_keys in this way, but that now
seems like overkill. It can have undesirable side-effects when the
array keys spuriously appear to advance, when in fact they were
restarted via the _bt_start_array_keys call, only to have their
original values restored via the finaltup call that immediately
follows.

Tying it back to your concern, once I do that (once I stop calling
_bt_start_array_keys in 0003-* to "hard reset" the arrays), I can also
stop caring about finaltup being set on the rightmost page, at the
point where we decide if _bt_skip_ikeyprefix should be called.

Attached is v29, which teaches _bt_skip_ikeyprefix to do things along
these lines (_bt_skip_ikeyprefix is added by 0003-*, same as last
time) . That's the first notable change for v29.

The code halts optimizing "prefix prechecks" when we notice a
non-equality key. It seems to me that we can do the precheck on shared
prefixes with non-equality keys just the same as with equality keys;
and it'd improve performance in those cases, too.

Yeah, I was thinking of doing this already (though not for RowCompare
inequalities, which would be hard to evaluate from here). It makes
sense because it's exactly the same case as the range skip array case,
really -- why not just do it the same way?

Second notable change for v29:

v29 also teaches 0003-* to handle plain inequalities (not just range
skip arrays) within _bt_skip_ikeyprefix, as I outlined to Matthias
here.

FWIW this doesn't really help much in practice, because scans that
benefit only do so to a limited degree, under fairly narrow
circumstances (I can explain what I mean by that if you're interested,
but it's not all that interesting). Even still, as I said the other
day, I think that it's worth doing this on consistency grounds. The
underlying rules that make this safe are literally identical to the
rules for range skip arrays (where determining if a key can be skipped
in _bt_skip_ikeyprefix tends to be much more important), so not doing
it with simple lower-order inequalities could confuse the reader.

Third notable change for v29:

Quite a few small improvements have been made to our new cost model,
in selfuncs.c/btcostestimate (see 0002-*, the main commit/patch that
introduces skip scan). The code is much better commented, and is more
idiomatic.

Most notably, I'm now using clauselist_selectivity() to adjust
ndistinct, as part of estimating the scan's num_sa_scans (I'm no
longer doing that in an ad-hoc way). The control flow is a lot easier
to understand. There are now a couple of new cases where we
conservatively fall back on using the old costing (i.e. we cost the
scan as if it was a Postgres 17 full index scan) for lack of a better
idea about what to do. We do this when we detect a default/generic
ndistinct estimate (we chicken out there), and we do it when we see a
range of values/a set of RestringInfos against a single column whose
selectivity is already relatively high (specifically, under
DEFAULT_RANGE_INEQ_SEL, meaning a selectivity that's under half a
percentage point).

Here are my plans around committing the patches:

* 0001-* can be committed within a matter of days. Probably before the
week is out.

It is commitable now, and is independently useful work.

* The remaining patches in the patch series (0002-* through to 0004-*)
are very close to being committable, but are still not quite ready.

I'm going to hold off on committing the big patches until late next
week, at the earliest. My current best estimate is that the bulk of
this work (0002-* through to 0004-*) will be committed together, on or
about April 2 (I suppose that I could commit 0004-* a few days later,
just to give things time to settle, but it wouldn't make sense to
commit 0002-* without also committing 0003-* immediately afterwards).

The tricky logic added by 0003-* is my single biggest outstanding
concern about the patch series. Particularly its potential to confuse
the existing invariants for required arrays. And particularly in light
of the changes that I made to 0003-* in the past few days. In short, I
need to think long and hard about the stuff I described under "Here's
how I think that this will be safe:" in my email to Matthias from
yesterday. The precondition and postcondition assertions in
_bt_advance_array_keys (added to Postgres 17) remain effective, which
gives me a certain amount of confidence in the changes made by 0003-*.

Thanks
--
Peter Geoghegan

Attachments:

v29-0001-Improve-nbtree-array-primitive-scan-scheduling.patchapplication/octet-stream; name=v29-0001-Improve-nbtree-array-primitive-scan-scheduling.patchDownload
From 857edd1fc62d2f3d8972b83715b9504befc41117 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Fri, 14 Feb 2025 16:11:59 -0500
Subject: [PATCH v29 1/5] Improve nbtree array primitive scan scheduling.

Add a new scheduling heuristic: don't end the ongoing primitive index
scan immediately (at the point where _bt_advance_array_keys notices that
the next set of matching tuples must be on a later page) if the primscan
already managed to step right/left from its first leaf page.  Schedule a
recheck against the next sibling leaf page's finaltup (do so by setting
so->ScanBehind) instead.

The heuristic tends to avoid scenarios where the top-level scan
repeatedly starts and ends primitive index scans that each read only one
leaf page from a group of neighboring leaf pages.  Affected top-level
scans will now tend to step forward (or backward) through the index,
without wasting very many cycles on descending the index anew.

The reschedule mechanism isn't exactly new.  But it was previously only
used to schedule a finaltup recheck once on the next leaf page, in order
to efficiently and correctly deal with a truncated high key (a high key
finaltup that creates uncertainty about how the scan's required array
keys track the progress of the top-level scan through the index's key
space).  The original version of the reschedule mechanism was added by
commit 5bf748b8, which invented the general concept of primitive scan
scheduling.  The mechanism was later enhanced by commit 79fa7b3b, which
taught it about cases involving inequality scan keys required in the
opposite-to-scan direction only (that were provisionally deemed to have
"satisfied" a high key's -inf truncated attribute value).  Now we the
reschedule mechanism is used without regard for the scan direction; it
isn't limited to forward scans anymore.

The theory behind the new heuristic is that any primitive scan that
makes it past its first leaf page is one that is already likely to have
arrays whose key values match index tuples that are closely clustered
together in the index.  The rules that determine whether we ever get
past the first page are still just as conservative as before.  The only
type of scan that can get past the first page without it being strictly
necessary are scans involving the aforementioned truncated high key
finaltup case.  Surviving past the first leaf page is a strong signal,
in and of itself.

Preparation for an upcoming patch that will add skip scan optimizations
to nbtree.  That'll work by adding skip arrays, which behave similarly
to SAOP arrays, but generate their elements procedurally and on-demand.

Note that this commit isn't specifically concerned with skip arrays; the
scheduling logic doesn't (and won't) condition anything on whether the
scan uses skip arrays, SAOP arrays, or some combination of the two
(which seems like a good general principle for _bt_advance_array_keys).
While the problems that this commit ameliorates are more likely with
skip arrays (at least in practice), SAOP arrays (or those with very
dense, contiguous array elements) are also affected.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Discussion: https://postgr.es/m/CAH2-Wzkz0wPe6+02kr+hC+JJNKfGtjGTzpG3CFVTQmKwWNrXNw@mail.gmail.com
---
 src/include/access/nbtree.h           |   9 +-
 src/backend/access/nbtree/nbtsearch.c |  70 ++++----
 src/backend/access/nbtree/nbtutils.c  | 227 ++++++++++++++------------
 3 files changed, 169 insertions(+), 137 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 0c43767f8..c9bc82eba 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1043,8 +1043,8 @@ typedef struct BTScanOpaqueData
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
-	bool		scanBehind;		/* Last array advancement matched -inf attr? */
-	bool		oppositeDirCheck;	/* explicit scanBehind recheck needed? */
+	bool		scanBehind;		/* arrays might be ahead of scan's key space? */
+	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
 	BTArrayKeyInfo *arrayKeys;	/* info about each equality-type array key */
 	FmgrInfo   *orderProcs;		/* ORDER procs for required equality keys */
 	MemoryContext arrayContext; /* scan-lifespan context for array data */
@@ -1099,6 +1099,7 @@ typedef struct BTReadPageState
 	 * Input and output parameters, set and unset by both _bt_readpage and
 	 * _bt_checkkeys to manage precheck optimizations
 	 */
+	bool		firstpage;		/* on first page of current primitive scan? */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
 
@@ -1298,8 +1299,8 @@ extern int	_bt_binsrch_array_skey(FmgrInfo *orderproc,
 extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir);
 extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 						  IndexTuple tuple, int tupnatts);
-extern bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
-								  IndexTuple finaltup);
+extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
+									 IndexTuple finaltup);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 22b27d01d..3fb0a0380 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -33,7 +33,7 @@ static OffsetNumber _bt_binsrch(Relation rel, BTScanInsert key, Buffer buf);
 static int	_bt_binsrch_posting(BTScanInsert key, Page page,
 								OffsetNumber offnum);
 static bool _bt_readpage(IndexScanDesc scan, ScanDirection dir,
-						 OffsetNumber offnum, bool firstPage);
+						 OffsetNumber offnum, bool firstpage);
 static void _bt_saveitem(BTScanOpaque so, int itemIndex,
 						 OffsetNumber offnum, IndexTuple itup);
 static int	_bt_setuppostingitems(BTScanOpaque so, int itemIndex,
@@ -1500,7 +1500,7 @@ _bt_next(IndexScanDesc scan, ScanDirection dir)
  */
 static bool
 _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
-			 bool firstPage)
+			 bool firstpage)
 {
 	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -1559,6 +1559,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.offnum = InvalidOffsetNumber;
 	pstate.skip = InvalidOffsetNumber;
 	pstate.continuescan = true; /* default assumption */
+	pstate.firstpage = firstpage;
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
 	pstate.rechecks = 0;
@@ -1604,7 +1605,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!firstPage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
@@ -1621,36 +1622,29 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	if (ScanDirectionIsForward(dir))
 	{
 		/* SK_SEARCHARRAY forward scans must provide high key up front */
-		if (arrayKeys && !P_RIGHTMOST(opaque))
+		if (arrayKeys)
 		{
-			ItemId		iid = PageGetItemId(page, P_HIKEY);
-
-			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
-
-			if (unlikely(so->oppositeDirCheck))
+			if (!P_RIGHTMOST(opaque))
 			{
-				Assert(so->scanBehind);
+				ItemId		iid = PageGetItemId(page, P_HIKEY);
 
-				/*
-				 * Last _bt_readpage call scheduled a recheck of finaltup for
-				 * required scan keys up to and including a > or >= scan key.
-				 *
-				 * _bt_checkkeys won't consider the scanBehind flag unless the
-				 * scan is stopped by a scan key required in the current scan
-				 * direction.  We need this recheck so that we'll notice when
-				 * all tuples on this page are still before the _bt_first-wise
-				 * start of matches for the current set of array keys.
-				 */
-				if (!_bt_oppodir_checkkeys(scan, dir, pstate.finaltup))
+				pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+				/* If a recheck is scheduled, check finaltup first */
+				if (unlikely(so->scanBehind) &&
+					!_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup))
 				{
 					/* Schedule another primitive index scan after all */
 					so->currPos.moreRight = false;
 					so->needPrimScan = true;
+					if (scan->parallel_scan)
+						_bt_parallel_primscan_schedule(scan,
+													   so->currPos.currPage);
 					return false;
 				}
-
-				/* Deliberately don't unset scanBehind flag just yet */
 			}
+
+			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
 		/* load items[] in ascending order */
@@ -1746,7 +1740,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 		 * only appear on non-pivot tuples on the right sibling page are
 		 * common.
 		 */
-		if (pstate.continuescan && !P_RIGHTMOST(opaque))
+		if (pstate.continuescan && !so->scanBehind && !P_RIGHTMOST(opaque))
 		{
 			ItemId		iid = PageGetItemId(page, P_HIKEY);
 			IndexTuple	itup = (IndexTuple) PageGetItem(page, iid);
@@ -1768,11 +1762,29 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	else
 	{
 		/* SK_SEARCHARRAY backward scans must provide final tuple up front */
-		if (arrayKeys && minoff <= maxoff && !P_LEFTMOST(opaque))
+		if (arrayKeys)
 		{
-			ItemId		iid = PageGetItemId(page, minoff);
+			if (minoff <= maxoff && !P_LEFTMOST(opaque))
+			{
+				ItemId		iid = PageGetItemId(page, minoff);
 
-			pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+				pstate.finaltup = (IndexTuple) PageGetItem(page, iid);
+
+				/* If a recheck is scheduled, check finaltup first */
+				if (unlikely(so->scanBehind) &&
+					!_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup))
+				{
+					/* Schedule another primitive index scan after all */
+					so->currPos.moreLeft = false;
+					so->needPrimScan = true;
+					if (scan->parallel_scan)
+						_bt_parallel_primscan_schedule(scan,
+													   so->currPos.currPage);
+					return false;
+				}
+			}
+
+			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
 		/* load items[] in descending order */
@@ -2276,14 +2288,14 @@ _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno,
 			if (ScanDirectionIsForward(dir))
 			{
 				/* note that this will clear moreRight if we can stop */
-				if (_bt_readpage(scan, dir, P_FIRSTDATAKEY(opaque), false))
+				if (_bt_readpage(scan, dir, P_FIRSTDATAKEY(opaque), seized))
 					break;
 				blkno = so->currPos.nextPage;
 			}
 			else
 			{
 				/* note that this will clear moreLeft if we can stop */
-				if (_bt_readpage(scan, dir, PageGetMaxOffsetNumber(page), false))
+				if (_bt_readpage(scan, dir, PageGetMaxOffsetNumber(page), seized))
 					break;
 				blkno = so->currPos.prevPage;
 			}
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index efe58beaa..4e455a66b 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -42,6 +42,8 @@ static bool _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 static bool _bt_verify_arrays_bt_first(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_verify_keys_with_arraykeys(IndexScanDesc scan);
 #endif
+static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
+								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
 							  bool advancenonrequired, bool prechecked, bool firstmatch,
@@ -870,15 +872,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	int			arrayidx = 0;
 	bool		beyond_end_advance = false,
 				has_required_opposite_direction_only = false,
-				oppodir_inequality_sktrig = false,
 				all_required_satisfied = true,
 				all_satisfied = true;
 
-	/*
-	 * Unset so->scanBehind (and so->oppositeDirCheck) in case they're still
-	 * set from back when we dealt with the previous page's high key/finaltup
-	 */
-	so->scanBehind = so->oppositeDirCheck = false;
+	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	if (sktrig_required)
 	{
@@ -990,18 +987,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			beyond_end_advance = true;
 			all_satisfied = all_required_satisfied = false;
 
-			/*
-			 * Set a flag that remembers that this was an inequality required
-			 * in the opposite scan direction only, that nevertheless
-			 * triggered the call here.
-			 *
-			 * This only happens when an inequality operator (which must be
-			 * strict) encounters a group of NULLs that indicate the end of
-			 * non-NULL values for tuples in the current scan direction.
-			 */
-			if (unlikely(required_opposite_direction_only))
-				oppodir_inequality_sktrig = true;
-
 			continue;
 		}
 
@@ -1306,10 +1291,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * Note: we don't just quit at this point when all required scan keys were
 	 * found to be satisfied because we need to consider edge-cases involving
 	 * scan keys required in the opposite direction only; those aren't tracked
-	 * by all_required_satisfied. (Actually, oppodir_inequality_sktrig trigger
-	 * scan keys are tracked by all_required_satisfied, since it's convenient
-	 * for _bt_check_compare to behave as if they are required in the current
-	 * scan direction to deal with NULLs.  We'll account for that separately.)
+	 * by all_required_satisfied.
 	 */
 	Assert(_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts,
 										false, 0, NULL) ==
@@ -1343,7 +1325,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	/*
 	 * When we encounter a truncated finaltup high key attribute, we're
 	 * optimistic about the chances of its corresponding required scan key
-	 * being satisfied when we go on to check it against tuples from this
+	 * being satisfied when we go on to recheck it against tuples from this
 	 * page's right sibling leaf page.  We consider truncated attributes to be
 	 * satisfied by required scan keys, which allows the primitive index scan
 	 * to continue to the next leaf page.  We must set so->scanBehind to true
@@ -1365,28 +1347,24 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 *
 	 * You can think of this as a speculative bet on what the scan is likely
 	 * to find on the next page.  It's not much of a gamble, though, since the
-	 * untruncated prefix of attributes must strictly satisfy the new qual
-	 * (though it's okay if any non-required scan keys fail to be satisfied).
+	 * untruncated prefix of attributes must strictly satisfy the new qual.
 	 */
-	if (so->scanBehind && has_required_opposite_direction_only)
+	if (so->scanBehind)
 	{
 		/*
-		 * However, we need to work harder whenever the scan involves a scan
-		 * key required in the opposite direction to the scan only, along with
-		 * a finaltup with at least one truncated attribute that's associated
-		 * with a scan key marked required (required in either direction).
+		 * Truncated high key -- _bt_scanbehind_checkkeys recheck scheduled.
 		 *
-		 * _bt_check_compare simply won't stop the scan for a scan key that's
-		 * marked required in the opposite scan direction only.  That leaves
-		 * us without an automatic way of reconsidering any opposite-direction
-		 * inequalities if it turns out that starting a new primitive index
-		 * scan will allow _bt_first to skip ahead by a great many leaf pages.
-		 *
-		 * We deal with this by explicitly scheduling a finaltup recheck on
-		 * the right sibling page.  _bt_readpage calls _bt_oppodir_checkkeys
-		 * for next page's finaltup (and we skip it for this page's finaltup).
+		 * Remember if recheck needs to call _bt_oppodir_checkkeys for next
+		 * page's finaltup (see below comments about "Handle inequalities
+		 * marked required in the opposite scan direction" for why).
 		 */
-		so->oppositeDirCheck = true;	/* recheck next page's high key */
+		so->oppositeDirCheck = has_required_opposite_direction_only;
+
+		/*
+		 * Make sure that any SAOP arrays that were not marked required by
+		 * preprocessing are reset to their first element for this direction
+		 */
+		_bt_rewind_nonrequired_arrays(scan, dir);
 	}
 
 	/*
@@ -1411,11 +1389,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * (primitive) scan.  If this happens at the start of a large group of
 	 * NULL values, then we shouldn't expect to be called again until after
 	 * the scan has already read indefinitely-many leaf pages full of tuples
-	 * with NULL suffix values.  We need a separate test for this case so that
-	 * we don't miss our only opportunity to skip over such a group of pages.
-	 * (_bt_first is expected to skip over the group of NULLs by applying a
-	 * similar "deduce NOT NULL" rule, where it finishes its insertion scan
-	 * key by consing up an explicit SK_SEARCHNOTNULL key.)
+	 * with NULL suffix values.  (_bt_first is expected to skip over the group
+	 * of NULLs by applying a similar "deduce NOT NULL" rule of its own, which
+	 * involves consing up an explicit SK_SEARCHNOTNULL key.)
 	 *
 	 * Apply a test against finaltup to detect and recover from the problem:
 	 * if even finaltup doesn't satisfy such an inequality, we just skip by
@@ -1423,20 +1399,18 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * that all of the tuples on the current page following caller's tuple are
 	 * also before the _bt_first-wise start of tuples for our new qual.  That
 	 * at least suggests many more skippable pages beyond the current page.
-	 * (when so->oppositeDirCheck was set, this'll happen on the next page.)
+	 * (when so->scanBehind and so->oppositeDirCheck are set, this'll happen
+	 * when we test the next page's finaltup/high key instead.)
 	 */
 	else if (has_required_opposite_direction_only && pstate->finaltup &&
-			 (all_required_satisfied || oppodir_inequality_sktrig) &&
 			 unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup)))
 	{
-		/*
-		 * Make sure that any non-required arrays are set to the first array
-		 * element for the current scan direction
-		 */
 		_bt_rewind_nonrequired_arrays(scan, dir);
 		goto new_prim_scan;
 	}
 
+continue_scan:
+
 	/*
 	 * Stick with the ongoing primitive index scan for now.
 	 *
@@ -1458,8 +1432,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	if (so->scanBehind)
 	{
 		/* Optimization: skip by setting "look ahead" mechanism's offnum */
-		Assert(ScanDirectionIsForward(dir));
-		pstate->skip = pstate->maxoff + 1;
+		if (ScanDirectionIsForward(dir))
+			pstate->skip = pstate->maxoff + 1;
+		else
+			pstate->skip = pstate->minoff - 1;
 	}
 
 	/* Caller's tuple doesn't match the new qual */
@@ -1469,6 +1445,41 @@ new_prim_scan:
 
 	Assert(pstate->finaltup);	/* not on rightmost/leftmost page */
 
+	/*
+	 * Looks like another primitive index scan is required.  But consider
+	 * backing out and continuing the primscan based on scan-level heuristics.
+	 *
+	 * Continue the ongoing primitive scan (but schedule a recheck for when
+	 * the scan arrives on the next sibling leaf page) when it has already
+	 * read at least one leaf page before the one we're reading now.  This is
+	 * important when reading subsets of an index with many distinct values in
+	 * respect of an attribute constrained by an array.  It encourages fewer,
+	 * larger primitive scans where that makes sense.
+	 *
+	 * Note: so->scanBehind is primarily used to indicate that the scan
+	 * encountered a finaltup that "satisfied" one or more required scan keys
+	 * on a truncated attribute value/-inf value.  We can safely reuse it to
+	 * force the scan to stay on the leaf level because the considerations are
+	 * exactly the same.
+	 *
+	 * Note: This heuristic isn't as aggressive as you might think.  We're
+	 * conservative about allowing a primitive scan to step from the first
+	 * leaf page it reads to the page's sibling page (we only allow it on
+	 * first pages whose finaltup strongly suggests that it'll work out).
+	 * Clearing this first page finaltup hurdle is a strong signal in itself.
+	 */
+	if (!pstate->firstpage)
+	{
+		/* Schedule a recheck once on the next (or previous) page */
+		so->scanBehind = true;
+		so->oppositeDirCheck = has_required_opposite_direction_only;
+
+		_bt_rewind_nonrequired_arrays(scan, dir);
+
+		/* Continue the current primitive scan after all */
+		goto continue_scan;
+	}
+
 	/*
 	 * End this primitive index scan, but schedule another.
 	 *
@@ -1499,7 +1510,7 @@ end_toplevel_scan:
 	 * first positions for what will then be the current scan direction.
 	 */
 	pstate->continuescan = false;	/* Tell _bt_readpage we're done... */
-	so->needPrimScan = false;	/* ...don't call _bt_first again, though */
+	so->needPrimScan = false;	/* ...and don't call _bt_first again */
 
 	/* Caller's tuple doesn't match any qual */
 	return false;
@@ -1634,6 +1645,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
+	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
 							arrayKeys, pstate->prechecked, pstate->firstmatch,
@@ -1688,62 +1700,36 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
+		/* Override _bt_check_compare, continue primitive scan */
+		pstate->continuescan = true;
+
 		/*
-		 * Tuple is still before the start of matches according to the scan's
-		 * required array keys (according to _all_ of its required equality
-		 * strategy keys, actually).
+		 * We will end up here repeatedly given a group of tuples > the
+		 * previous array keys and < the now-current keys (for a backwards
+		 * scan it's just the same, though the operators swap positions).
 		 *
-		 * _bt_advance_array_keys occasionally sets so->scanBehind to signal
-		 * that the scan's current position/tuples might be significantly
-		 * behind (multiple pages behind) its current array keys.  When this
-		 * happens, we need to be prepared to recover by starting a new
-		 * primitive index scan here, on our own.
+		 * We must avoid allowing this linear search process to scan very many
+		 * tuples from well before the start of tuples matching the current
+		 * array keys (or from well before the point where we'll once again
+		 * have to advance the scan's array keys).
+		 *
+		 * We keep the overhead under control by speculatively "looking ahead"
+		 * to later still-unscanned items from this same leaf page.  We'll
+		 * only attempt this once the number of tuples that the linear search
+		 * process has examined starts to get out of hand.
 		 */
-		Assert(!so->scanBehind ||
-			   so->keyData[ikey].sk_strategy == BTEqualStrategyNumber);
-		if (unlikely(so->scanBehind) && pstate->finaltup &&
-			_bt_tuple_before_array_skeys(scan, dir, pstate->finaltup, tupdesc,
-										 BTreeTupleGetNAtts(pstate->finaltup,
-															scan->indexRelation),
-										 false, 0, NULL))
+		pstate->rechecks++;
+		if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
 		{
-			/* Cut our losses -- start a new primitive index scan now */
-			pstate->continuescan = false;
-			so->needPrimScan = true;
-		}
-		else
-		{
-			/* Override _bt_check_compare, continue primitive scan */
-			pstate->continuescan = true;
+			/* See if we should skip ahead within the current leaf page */
+			_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
 
 			/*
-			 * We will end up here repeatedly given a group of tuples > the
-			 * previous array keys and < the now-current keys (for a backwards
-			 * scan it's just the same, though the operators swap positions).
-			 *
-			 * We must avoid allowing this linear search process to scan very
-			 * many tuples from well before the start of tuples matching the
-			 * current array keys (or from well before the point where we'll
-			 * once again have to advance the scan's array keys).
-			 *
-			 * We keep the overhead under control by speculatively "looking
-			 * ahead" to later still-unscanned items from this same leaf page.
-			 * We'll only attempt this once the number of tuples that the
-			 * linear search process has examined starts to get out of hand.
+			 * Might have set pstate.skip to a later page offset.  When that
+			 * happens then _bt_readpage caller will inexpensively skip ahead
+			 * to a later tuple from the same page (the one just after the
+			 * tuple we successfully "looked ahead" to).
 			 */
-			pstate->rechecks++;
-			if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS)
-			{
-				/* See if we should skip ahead within the current leaf page */
-				_bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc);
-
-				/*
-				 * Might have set pstate.skip to a later page offset.  When
-				 * that happens then _bt_readpage caller will inexpensively
-				 * skip ahead to a later tuple from the same page (the one
-				 * just after the tuple we successfully "looked ahead" to).
-				 */
-			}
 		}
 
 		/* This indextuple doesn't match the current qual, in any case */
@@ -1760,6 +1746,39 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 								  ikey, true);
 }
 
+/*
+ * Test whether finaltup (the final tuple on the page) is still before the
+ * start of matches for the current array keys.
+ *
+ * Caller's finaltup tuple is the page high key (for forwards scans), or the
+ * first non-pivot tuple (for backwards scans).  Called during scans with
+ * array keys when the so->scanBehind flag was set on the previous page.
+ *
+ * Returns false if the tuple is still before the start of matches.  When that
+ * happens caller should cut its losses and start a new primitive index scan.
+ * Otherwise returns true.
+ */
+bool
+_bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
+						 IndexTuple finaltup)
+{
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	int			nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+
+	Assert(so->numArrayKeys);
+
+	if (_bt_tuple_before_array_skeys(scan, dir, finaltup, tupdesc,
+									 nfinaltupatts, false, 0, NULL))
+		return false;
+
+	if (!so->oppositeDirCheck)
+		return true;
+
+	return _bt_oppodir_checkkeys(scan, dir, finaltup);
+}
+
 /*
  * Test whether an indextuple fails to satisfy an inequality required in the
  * opposite direction only.
@@ -1778,7 +1797,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
  * _bt_checkkeys to stop the scan to consider array advancement/starting a new
  * primitive index scan.
  */
-bool
+static bool
 _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 					  IndexTuple finaltup)
 {
-- 
2.47.2

v29-0004-Apply-low-order-skip-key-in-_bt_first-more-often.patchapplication/octet-stream; name=v29-0004-Apply-low-order-skip-key-in-_bt_first-more-often.patchDownload
From 1c83fbe2fad46ab73603e8f704b5c6f1a4c3fc12 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 13 Jan 2025 16:08:32 -0500
Subject: [PATCH v29 4/5] Apply low-order skip key in _bt_first more often.

Convert low_compare and high_compare nbtree skip array inequalities
(with opclasses that offer skip support) such that _bt_first is
consistently able to use later keys when descending the tree within
_bt_first.

For example, an index qual "WHERE a > 5 AND b = 2" is now converted to
"WHERE a >= 6 AND b = 2" by a new preprocessing step that takes place
after a final low_compare and/or high_compare are chosen by all earlier
preprocessing steps.  That way the scan's initial call to _bt_first will
use "WHERE a >= 6 AND b = 2" to find the initial leaf level position,
rather than merely using "WHERE a > 5" -- "b = 2" can always be applied.
This has a decent chance of making the scan avoid an extra _bt_first
call that would otherwise be needed just to determine the lowest-sorting
"a" value in the index (the lowest that still satisfies "WHERE a > 5").

The transformation process can only lower the total number of index
pages read when the use of a more restrictive set of initial positioning
keys in _bt_first actually allows the scan to land on some later leaf
page directly, relative to the unoptimized case (or on an earlier leaf
page directly, when scanning backwards).  The savings can be far greater
when affected skip arrays come after some higher-order array.  For
example, a qual "WHERE x IN (1, 2, 3) AND y > 5 AND z = 2" can now save
as many as 3 _bt_first calls as a result of these transformations (there
can be as many as 1 _bt_first call saved per "x" array element).

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=FJ78K3WsF3iWNxWnUCY9f=Jdg3QPxaXE=uYUbmuRz5Q@mail.gmail.com
---
 src/backend/access/nbtree/nbtpreprocesskeys.c | 181 ++++++++++++++++++
 src/test/regress/expected/create_index.out    |  21 ++
 src/test/regress/sql/create_index.sql         |  10 +
 3 files changed, 212 insertions(+)

diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 04792eab8..999bb4578 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -50,6 +50,12 @@ static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
 								 BTArrayKeyInfo *array, bool *qual_ok);
 static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
 								 BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+									   BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
@@ -1290,6 +1296,172 @@ _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
 	return true;
 }
 
+/*
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts their
+ * operator strategies, while changing the operator as appropriate).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value MINVAL, using a transformed >= key
+ * instead of using the original > key makes it safe to include lower-order
+ * scan keys in the insertion scan key (there must be lower-order scan keys
+ * after the skip array).  We will avoid an extra _bt_first to find the first
+ * value in the index > sk_argument -- at least when the first real matching
+ * value in the index happens to be an exact match for the sk_argument value
+ * that we produced here by incrementing the original input key's sk_argument.
+ * (Backwards scans derive the same benefit when they encounter the sentinel
+ * value MAXVAL, by converting the high_compare key from < to <=.)
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01'.
+ *
+ * Note: We can safely modify array->low_compare/array->high_compare in place
+ * because they just point to copies of our scan->keyData[] input scan keys
+ * (namely the copies returned by _bt_preprocess_array_keys to be used as
+ * input into the standard preprocessing steps in _bt_preprocess_keys).
+ * Everything will be reset if there's a rescan.
+ */
+static void
+_bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+						   BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	/*
+	 * Called last among all preprocessing steps, when the skip array's final
+	 * low_compare and high_compare have both been chosen
+	 */
+	Assert(so->skipScan);
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1 && !array->null_elem && array->sksup);
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skiparray_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skiparray_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1825,6 +1997,15 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && array->sksup &&
+						!array->null_elem)
+						_bt_skiparray_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 15dde752f..fa4517e2d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2589,6 +2589,27 @@ ORDER BY thousand;
         1 |     1001
 (1 row)
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+                                            QUERY PLAN                                            
+--------------------------------------------------------------------------------------------------
+ Limit
+   ->  Index Only Scan using tenk1_thous_tenthous on tenk1
+         Index Cond: ((thousand > '-1'::integer) AND (tenthous = ANY ('{1001,3000}'::integer[])))
+(3 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+        1 |     1001
+(2 rows)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index 40ba3d65b..e8c16959a 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -993,6 +993,16 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
 ORDER BY thousand;
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
-- 
2.47.2

v29-0002-Add-nbtree-skip-scan-optimizations.patchapplication/octet-stream; name=v29-0002-Add-nbtree-skip-scan-optimizations.patchDownload
From d67800c3cde566abcffb8f38f871805caee5d8eb Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v29 2/5] Add nbtree skip scan optimizations.

Teach nbtree composite index scans to opportunistically skip over
irrelevant sections of composite indexes given a query with an omitted
prefix column.  When nbtree is passed input scan keys derived from a
query predicate "WHERE b = 5", new nbtree preprocessing steps now output
"WHERE a = ANY(<every possible 'a' value>) AND b = 5" scan keys.  That
is, preprocessing generates a "skip array" (along with an associated
scan key) for the omitted column "a", which makes it safe to mark the
scan key on "b" as required to continue the scan.  This is far more
efficient than a traditional full index scan whenever it allows the scan
to skip over many irrelevant leaf pages, by iteratively repositioning
itself using the keys on "a" and "b" together.

A skip array has "elements" that are generated procedurally and on
demand, but otherwise works just like a regular ScalarArrayOp array.
Preprocessing can freely add a skip array before or after any input
ScalarArrayOp arrays.  Index scans with a skip array decide when and
where to reposition the scan using the same approach as any other scan
with array keys.  This design builds on the design for array advancement
and primitive scan scheduling added to Postgres 17 by commit 5bf748b8.

The core B-Tree operator classes on most discrete types generate their
array elements with the help of their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the next
possible indexable value frequently turns out to be the next value
stored in the index.  Opclasses that lack a skip support routine fall
back on having nbtree "increment" (or "decrement") a skip array's
current element by setting the NEXT (or PRIOR) scan key flag, without
directly changing the scan key's sk_argument.  These sentinel values
behave just like any other value from an array -- though they can never
locate equal index tuples (they can only locate the next group of index
tuples containing the next set of non-sentinel values that the scan's
arrays need to advance to).

Inequality scan keys can affect how skip arrays generate their values.
Their range is constrained by the inequalities.  For example, a skip
array on "a" will only use element values 1 and 2 given a qual such as
"WHERE a BETWEEN 1 AND 2 AND b = 66".  A scan using such a skip array
has almost identical performance characteristics to one with the qual
"WHERE a = ANY('{1, 2}') AND b = 66".  The scan will be much faster when
it can be executed as two selective primitive index scans instead of a
single very large index scan that reads many irrelevant leaf pages.
However, the array transformation process won't always lead to improved
performance at runtime.  Much depends on physical index characteristics.

B-Tree preprocessing is optimistic about skipping working out: it
applies static, generic rules when determining where to generate skip
arrays, which assumes that the runtime overhead of maintaining skip
arrays will pay for itself -- or lead to only a modest performance loss.
As things stand, these assumptions are much too optimistic: skip array
maintenance will lead to unacceptable regressions with unsympathetic
queries (queries whose scan has little to no chance of avoid reading
irrelevant leaf pages).  An upcoming commit will address the problems in
this area by adding a mechanism that greatly lowers the cost of array
maintenance in these unfavorable cases.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |   3 +-
 src/include/access/nbtree.h                   |  35 +-
 src/include/catalog/pg_amproc.dat             |  22 +
 src/include/catalog/pg_proc.dat               |  27 +
 src/include/utils/skipsupport.h               |  98 +++
 src/backend/access/index/indexam.c            |   3 +-
 src/backend/access/nbtree/nbtcompare.c        | 273 +++++++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 600 ++++++++++++--
 src/backend/access/nbtree/nbtree.c            | 196 ++++-
 src/backend/access/nbtree/nbtsearch.c         | 109 ++-
 src/backend/access/nbtree/nbtutils.c          | 754 ++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |   4 +
 src/backend/commands/opclasscmds.c            |  25 +
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/date.c                  |  46 ++
 src/backend/utils/adt/meson.build             |   1 +
 src/backend/utils/adt/selfuncs.c              | 490 +++++++++---
 src/backend/utils/adt/skipsupport.c           |  61 ++
 src/backend/utils/adt/timestamp.c             |  48 ++
 src/backend/utils/adt/uuid.c                  |  70 ++
 doc/src/sgml/btree.sgml                       |  34 +-
 doc/src/sgml/indexam.sgml                     |   3 +-
 doc/src/sgml/indices.sgml                     |  49 +-
 doc/src/sgml/monitoring.sgml                  |   4 +-
 doc/src/sgml/perform.sgml                     |  31 +
 doc/src/sgml/xindex.sgml                      |  16 +-
 src/test/regress/expected/alter_generic.out   |  10 +-
 src/test/regress/expected/btree_index.out     |  41 +
 src/test/regress/expected/create_index.out    | 183 ++++-
 src/test/regress/expected/psql.out            |   3 +-
 src/test/regress/sql/alter_generic.sql        |   5 +-
 src/test/regress/sql/btree_index.sql          |  21 +
 src/test/regress/sql/create_index.sql         |  63 +-
 src/tools/pgindent/typedefs.list              |   3 +
 34 files changed, 2971 insertions(+), 361 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c4a073773..52916bab7 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -214,7 +214,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index c9bc82eba..e4296c9c5 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -707,6 +708,10 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
  *	(BTOPTIONS_PROC).  These procedures define a set of user-visible
  *	parameters that can be used to control operator class behavior.  None of
  *	the built-in B-Tree operator classes currently register an "options" proc.
+ *
+ *	To facilitate more efficient B-Tree skip scans, an operator class may
+ *	choose to offer a sixth amproc procedure (BTSKIPSUPPORT_PROC).  For full
+ *	details, see src/include/utils/skipsupport.h.
  */
 
 #define BTORDER_PROC		1
@@ -714,7 +719,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1027,10 +1033,21 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
-	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for standard ScalarArrayOpExpr arrays */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+	int			cur_elem;		/* index of current element in elem_values */
+
+	/* fields for skip arrays, which generate element datums procedurally */
+	int16		attlen;			/* attr len in bytes */
+	bool		attbyval;		/* as FormData_pg_attribute.attbyval */
+	bool		null_elem;		/* NULL is lowest/highest element? */
+	SkipSupport sksup;			/* skip support (NULL if opclass lacks it) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1042,6 +1059,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* arrays might be ahead of scan's key space? */
 	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
@@ -1119,6 +1137,15 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on column without input = */
+
+/* SK_BT_SKIP-only flags (set and unset by array advancement) */
+#define SK_BT_MINVAL	0x00080000	/* invalid sk_argument, use low_compare */
+#define SK_BT_MAXVAL	0x00100000	/* invalid sk_argument, use high_compare */
+#define SK_BT_NEXT		0x00200000	/* positions the scan > sk_argument */
+#define SK_BT_PRIOR		0x00400000	/* positions the scan < sk_argument */
+
+/* Remaps pg_index flag bits to uppermost SK_BT_* byte */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1165,7 +1192,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 19100482b..37000639f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 890822eaf..083da24af 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2288,6 +2303,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4478,6 +4496,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6357,6 +6378,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9405,6 +9429,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..bc51847cf
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining groups of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * It usually isn't feasible to implement skip support for an opclass whose
+ * input type is continuous.  The B-Tree code falls back on next-key sentinel
+ * values for any opclass that doesn't provide its own skip support function.
+ * This isn't really an implementation restriction; there is no benefit to
+ * providing skip support for an opclass where guessing that the next indexed
+ * value is the next possible indexable value never (or hardly ever) works out.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order)
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 * Operator class decrement/increment functions will never be called with
+	 * a NULL "existing" argument, either.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern SkipSupport PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+												 bool reverse);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 55ec4c103..219df1971 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -489,7 +489,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	if (parallel_aware &&
 		indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 291cb8fc1..4da5a3c1d 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 38a87af1c..04792eab8 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -45,8 +45,15 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
+static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
+								 ScanKey skey, FmgrInfo *orderproc,
+								 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
+								 BTArrayKeyInfo *array, bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
+							   int *numSkipArrayKeys);
 static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
 									  Oid elemtype, StrategyNumber strat,
 									  Datum *elems, int nelems);
@@ -89,6 +96,8 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -101,10 +110,16 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never generated skip array scan keys, it would be possible for "gaps"
+ * to appear that make it unsafe to mark any subsequent input scan keys
+ * (copied from scan->keyData[]) as required to continue the scan.  Prior to
+ * Postgres 18, a qual like "WHERE y = 4" always required a full index scan.
+ * It'll now become "WHERE x = ANY('{every possible x value}') and y = 4" on
+ * output, due to the addition of a skip array on "x".  This has the potential
+ * to be much more efficient than a full index scan (though it'll behave like
+ * a full index scan when there are many distinct "x" values).
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -141,7 +156,12 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -200,6 +220,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProcs[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -231,6 +259,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				   (so->arrayKeys[0].scan_key == 0 &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
+		Assert(!so->skipScan);
 
 		return;
 	}
@@ -288,7 +317,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -345,7 +375,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -393,6 +422,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, _bt_preprocess_array_keys has already added "=" keys
+			 * sufficient to form an unbroken series of "=" constraints on all
+			 * attrs prior to the attr from the final scan->keyData[] key.
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -481,6 +515,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -489,6 +524,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -508,8 +544,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -803,6 +837,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -812,6 +849,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -885,6 +938,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -978,24 +1032,55 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
  * Compare an array scan key to a scalar scan key, eliminating contradictory
  * array elements such that the scalar scan key becomes redundant.
  *
+ * If the opfamily is incomplete we may not be able to determine which
+ * elements are contradictory.  When we return true we'll have validly set
+ * *qual_ok, guaranteeing that at least the scalar scan key can be considered
+ * redundant.  We return false if the comparison could not be made (caller
+ * must keep both scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar scan keys.
+ */
+static bool
+_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
+							   bool *qual_ok)
+{
+	Assert(arraysk->sk_attno == skey->sk_attno);
+	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
+		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
+	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
+		   skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Just call the appropriate helper function based on whether it's a SAOP
+	 * array or a skip array.  Both helpers will set *qual_ok in passing.
+	 */
+	if (array->num_elems != -1)
+		return _bt_saoparray_shrink(scan, arraysk, skey, orderproc, array,
+									qual_ok);
+	else
+		return _bt_skiparray_shrink(scan, skey, array, qual_ok);
+}
+
+/*
+ * Preprocessing of SAOP (non-skip) array scan key, used to determine which
+ * array elements are eliminated as contradictory by a non-array scalar key.
+ * _bt_compare_array_scankey_args helper function.
+ *
  * Array elements can be eliminated as contradictory when excluded by some
  * other operator on the same attribute.  For example, with an index scan qual
  * "WHERE a IN (1, 2, 3) AND a < 2", all array elements except the value "1"
  * are eliminated, and the < scan key is eliminated as redundant.  Cases where
  * every array element is eliminated by a redundant scalar scan key have an
  * unsatisfiable qual, which we handle by setting *qual_ok=false for caller.
- *
- * If the opfamily doesn't supply a complete set of cross-type ORDER procs we
- * may not be able to determine which elements are contradictory.  If we have
- * the required ORDER proc then we return true (and validly set *qual_ok),
- * guaranteeing that at least the scalar scan key can be considered redundant.
- * We return false if the comparison could not be made (caller must keep both
- * scan keys when this happens).
  */
 static bool
-_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
-							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
-							   bool *qual_ok)
+_bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+					 FmgrInfo *orderproc, BTArrayKeyInfo *array, bool *qual_ok)
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
@@ -1006,14 +1091,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
-	Assert(arraysk->sk_attno == skey->sk_attno);
 	Assert(array->num_elems > 0);
-	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
-		   arraysk->sk_strategy == BTEqualStrategyNumber);
-	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
-		   skey->sk_strategy != BTEqualStrategyNumber);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
 
 	/*
 	 * _bt_binsrch_array_skey searches an array for the entry best matching a
@@ -1112,6 +1191,105 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	return true;
 }
 
+/*
+ * Preprocessing of skip (non-SAOP) array scan key, used to determine
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function.
+ *
+ * Unlike _bt_saoparray_shrink, we don't modify caller's array in-place.  Skip
+ * arrays work by procedurally generating their elements as needed, so we just
+ * store the inequality as the skip array's low_compare or high_compare.  The
+ * array's elements will be generated from the range of values that satisfies
+ * both low_compare and high_compare.
+ */
+static bool
+_bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
+					 bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Array's index attribute will be constrained by a strict operator/key.
+	 * Array must not "contain a NULL element" (i.e. the scan must not apply
+	 * "IS NULL" qual when it reaches the end of the index that stores NULLs).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Consider if we should treat caller's scalar scan key as the skip
+	 * array's high_compare or low_compare.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way the scan can always safely assume that
+	 * it's okay to use the input-opclass-only-type proc from so->orderProcs[]
+	 * (they can be cross-type with SAOP arrays, but never with skip arrays).
+	 *
+	 * This approach is enabled by MINVAL/MAXVAL sentinel key markings, which
+	 * can be thought of as representing either the lowest or highest matching
+	 * array element (excluding the NULL element, where applicable, though as
+	 * just discussed it isn't applicable to this range skip array anyway).
+	 * Array keys marked MINVAL/MAXVAL never have a valid datum in their
+	 * sk_argument field.  The scan directly applies the array's low_compare
+	 * key when it encounters MINVAL in the array key proper (just as it
+	 * applies high_compare when it sees MAXVAL set in the array key proper).
+	 * The scan must never use the array's so->orderProcs[] proc against
+	 * low_compare's/high_compare's sk_argument, either (so->orderProcs[] is
+	 * only intended to be used with rhs datums from the array proper/index).
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* replace existing high_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing high_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's high_compare */
+			array->high_compare = skey;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* replace existing low_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing low_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's low_compare */
+			array->low_compare = skey;
+			break;
+		case BTEqualStrategyNumber:
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1137,6 +1315,12 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -1152,41 +1336,36 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	Oid			skip_eq_ops[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
-	ScanKey		cur;
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
+	so->skipScan = (numSkipArrayKeys > 0);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys we'll need to generate below
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -1201,18 +1380,20 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
+		ScanKey		inkey = scan->keyData + input_ikey,
+					cur;
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
 		Oid			elemtype;
@@ -1225,21 +1406,113 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		Datum	   *elem_values;
 		bool	   *elem_nulls;
 		int			num_nonnulls;
-		int			j;
+
+		/* set up next output scan key */
+		cur = &arrayKeyData[numArrayKeyData];
+
+		/* Backfill skip arrays for attrs < or <= input key's attr? */
+		while (numSkipArrayKeys && attno_skip <= inkey->sk_attno)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skip_eq_ops[attno_skip - 1];
+			CompactAttribute *attr;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/*
+				 * Attribute already has an = input key, so don't output a
+				 * skip array for attno_skip.  Just copy attribute's = input
+				 * key into arrayKeyData[] once outside this inner loop.
+				 *
+				 * Note: When we get here there must be a later attribute that
+				 * lacks an equality input key, and still needs a skip array
+				 * (if there wasn't then numSkipArrayKeys would be 0 by now).
+				 */
+				Assert(attno_skip == inkey->sk_attno);
+				/* inkey can't be last input key to be marked required: */
+				Assert(input_ikey < scan->numberOfKeys - 1);
+#if 0
+				/* Could be a redundant input scan key, so can't do this: */
+				Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+					   (inkey->sk_flags & SK_SEARCHNULL));
+#endif
+
+				attno_skip++;
+				break;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize generic BTArrayKeyInfo fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+
+			/* Initialize skip array specific BTArrayKeyInfo fields */
+			attr = TupleDescCompactAttr(RelationGetDescr(rel), attno_skip - 1);
+			reverse = (indoption[attno_skip - 1] & INDOPTION_DESC) != 0;
+			so->arrayKeys[numArrayKeys].attlen = attr->attlen;
+			so->arrayKeys[numArrayKeys].attbyval = attr->attbyval;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup =
+				PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse);
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * We'll need a 3-way ORDER proc.  Set that up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			numArrayKeys++;
+			numArrayKeyData++;	/* keep this scan key/array */
+
+			/* set up next output scan key */
+			cur = &arrayKeyData[numArrayKeyData];
+
+			/* remember having output this skip array and scan key */
+			numSkipArrayKeys--;
+			attno_skip++;
+		}
 
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
-		*cur = scan->keyData[input_ikey];
+		*cur = *inkey;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -1257,7 +1530,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * all btree operators are strict.
 		 */
 		num_nonnulls = 0;
-		for (j = 0; j < num_elems; j++)
+		for (int j = 0; j < num_elems; j++)
 		{
 			if (!elem_nulls[j])
 				elem_values[num_nonnulls++] = elem_values[j];
@@ -1295,7 +1568,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -1306,7 +1579,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -1323,7 +1596,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -1392,23 +1665,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			origelemtype = elemtype;
 		}
 
-		/*
-		 * And set up the BTArrayKeyInfo data.
-		 *
-		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
-		 * scan_key field later on, after so->keyData[] has been finalized.
-		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		/* Initialize generic BTArrayKeyInfo fields */
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
+
+		/* Initialize SAOP array specific BTArrayKeyInfo fields */
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].cur_elem = -1;	/* i.e. invalid */
+
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
+	Assert(numSkipArrayKeys == 0);
+
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -1514,7 +1788,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -1575,6 +1850,189 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit every so->arrayKeys[] entry.
+ *
+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller must add this many
+ * skip arrays to each of the most significant attributes lacking any keys
+ * that use the = strategy (IS NULL keys count as = keys here).  The specific
+ * attributes that need skip arrays are indicated by initializing caller's
+ * skip_eq_ops[] 0-based attribute offset to a valid = op strategy Oid.  We'll
+ * only ever set skip_eq_ops[] entries to InvalidOid for attributes that
+ * already have an equality key in scan->keyData[] input keys -- and only when
+ * there's some later "attribute gap" for us to "fill-in" with a skip array.
+ *
+ * We're optimistic about skipping working out: we always add exactly the skip
+ * arrays needed to maximize the number of input scan keys that can ultimately
+ * be marked as required to continue the scan (but no more).  For a composite
+ * index on (a, b, c, d), we'll instruct caller to add skip arrays as follows:
+ *
+ * Input keys						Output keys (after all preprocessing)
+ * ----------						-------------------------------------
+ * a = 1							a = 1 (no skip arrays)
+ * a = ANY('{0, 1}')				a = ANY('{0, 1}') (no skip arrays)
+ * b = 42							skip a AND b = 42
+ * b = ANY('{40, 42}')				skip a AND b = ANY('{40, 42}')
+ * a = 1 AND b = 42					a = 1 AND b = 42 (no skip arrays)
+ * a >= 1 AND b = 42				range skip a AND b = 42
+ * a = 1 AND b > 42					a = 1 AND b > 42 (no skip arrays)
+ * a >= 1 AND a <= 3 AND b = 42		range skip a AND b = 42
+ * a = 1 AND c <= 27				a = 1 AND skip b AND c <= 27
+ * a = 1 AND d >= 1					a = 1 AND skip b AND skip c AND d >= 1
+ * a = 1 AND b >= 42 AND d > 1		a = 1 AND range skip b AND skip c AND d > 1
+ */
+static int
+_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_skip = 1,
+				attno_inkey = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+#ifdef DEBUG_DISABLE_SKIP_SCAN
+	/* don't attempt to add skip arrays */
+	return numArrayKeys;
+#endif
+
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+			/* Look up input opclass's equality operator (might fail) */
+			skip_eq_ops[attno_skip - 1] =
+				get_opfamily_member(opfamily, opcintype, opcintype,
+									BTEqualStrategyNumber);
+			if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key
+		 * (doesn't matter whether that attribute has an equality key, or just
+		 * uses inequality keys).
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys
+			 */
+			if (attno_has_equal)
+			{
+				/* Attributes with an = key must have InvalidOid eq_op set */
+				skip_eq_ops[attno_skip - 1] = InvalidOid;
+			}
+			else
+			{
+				Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+				Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+				/* Look up input opclass's equality operator (might fail) */
+				skip_eq_ops[attno_skip - 1] =
+					get_opfamily_member(opfamily, opcintype, opcintype,
+										BTEqualStrategyNumber);
+
+				if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+				{
+					/*
+					 * Input opclass lacks an equality strategy operator, so
+					 * don't generate a skip array that definitely won't work
+					 */
+					break;
+				}
+
+				/* plan on adding a backfill skip array for this attribute */
+				(*numSkipArrayKeys)++;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys include any equality strategy
+		 * scan keys (IS NULL keys count as equality keys here)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required later on.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
 /*
  * _bt_find_extreme_element() -- get least or greatest array element
  *
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index c0a8833e0..40f4e6eb2 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -30,6 +30,7 @@
 #include "storage/indexfsm.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -75,14 +76,26 @@ typedef struct BTParallelScanDescData
 
 	/*
 	 * btps_arrElems is used when scans need to schedule another primitive
-	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
+	 * index scan with one or more SAOP arrays.  Holds BTArrayKeyInfo.cur_elem
+	 * offsets for each = scan key associated with a ScalarArrayOp array.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * Additional space (at the end of the struct) is used when scans need to
+	 * schedule another primitive index scan with one or more skip arrays.
+	 * Holds a flattened datum representation for each = scan key associated
+	 * with a skip array.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -335,6 +348,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	else
 		so->keyData = NULL;
 
+	so->skipScan = false;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->oppositeDirCheck = false;
@@ -540,10 +554,166 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume that every input scan key will be output with
+	 * its own SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * array (and an associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		CompactAttribute *attr;
+
+		/*
+		 * We make the conservative assumption that every index column will
+		 * also require a skip array.
+		 *
+		 * Every skip array must have space to store its scan key's sk_flags.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		/* Consider space required to store a datum of opclass input type */
+		attr = TupleDescCompactAttr(rel->rd_att, attnum - 1);
+		if (attr->attbyval)
+		{
+			/* This index attribute stores pass-by-value datums */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  true, attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * This index attribute stores pass-by-reference datums.
+		 *
+		 * Assume that serializing this array will use just as much space as a
+		 * pass-by-value datum, in addition to space for the largest possible
+		 * whole index tuple (this is not just a per-datum portion of the
+		 * largest possible tuple because that'd be almost as large anyway).
+		 *
+		 * This is quite conservative, but it's not clear how we could do much
+		 * better.  The executor requires an up-front storage request size
+		 * that reliably covers the scan's high watermark memory usage.  We
+		 * can't be sure of the real high watermark until the scan is over.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BTMaxItemSize);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   array->attbyval, array->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array key, while freeing memory allocated for old
+		 * sk_argument where required
+		 */
+		if (!array->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -612,6 +782,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -678,14 +849,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -830,6 +996,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -848,12 +1015,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
 	LWLockRelease(&btscan->btps_lock);
 }
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 3fb0a0380..b4f72d82d 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -965,6 +965,15 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  All
+	 * array keys are always considered = keys, but we'll sometimes need to
+	 * treat the current key value as if we were using an inequality strategy.
+	 * This happens whenever a skip array's scan key is found to have been set
+	 * to one of the special sentinel values representing an imaginary point
+	 * before/after some (or all) of the index's real indexable values.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1040,8 +1049,56 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is MINVAL, choose low_compare (when scanning
+				 * backwards it'll be MAXVAL, and we'll choose high_compare).
+				 *
+				 * Note: if the array's low_compare key makes 'chosen' NULL,
+				 * then we behave as if the array's first element is -inf,
+				 * except when !array->null_elem implies a usable NOT NULL
+				 * constraint.
+				 */
+				if (chosen != NULL &&
+					(chosen->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skipequalitykey = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					Assert(so->skipScan);
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MAXVAL));
+						chosen = array->low_compare;
+					}
+					else
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MINVAL));
+						chosen = array->high_compare;
+					}
+
+					Assert(chosen == NULL ||
+						   chosen->sk_attno == skipequalitykey->sk_attno);
+
+					if (!array->null_elem)
+						impliesNN = skipequalitykey;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1084,9 +1141,41 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 
 				/*
-				 * Done if that was the last attribute, or if next key is not
-				 * in sequence (implying no boundary key is available for the
-				 * next attribute).
+				 * If the key that we just added to startKeys[] is a skip
+				 * array = key whose current element is marked NEXT or PRIOR,
+				 * make strat_total > or < (and stop adding boundary keys).
+				 * This can only happen with opclasses that lack skip support.
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(so->skipScan);
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(strat_total == BTEqualStrategyNumber);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We're done.  We'll never find an exact = match for a
+					 * NEXT or PRIOR sentinel sk_argument value.  There's no
+					 * sense in trying to add more keys to startKeys[].
+					 */
+					break;
+				}
+
+				/*
+				 * Done if that was the last scan key output by preprocessing.
+				 * Also done if there is a gap index attribute that lacks a
+				 * usable key (only possible when preprocessing was unable to
+				 * generate a skip array key to "fill in the gap").
 				 */
 				if (i >= so->numberOfKeys ||
 					cur->sk_attno != curattr + 1)
@@ -1578,10 +1667,10 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * corresponding value from the last item on the page.  So checking with
 	 * the last item on the page would give a more precise answer.
 	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
+	 * We don't do this for the first page read by each (primitive) scan, to
+	 * avoid slowing down point queries.  They typically don't stand to gain
+	 * much when the optimization can be applied, and are more likely to
+	 * notice the overhead of the precheck.  Also avoid it during skip scans.
 	 *
 	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
 	 * just set a low-order required array's key to the best available match
@@ -1605,7 +1694,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * required < or <= strategy scan keys) during the precheck, we can safely
 	 * assume that this must also be true of all earlier tuples from the page.
 	 */
-	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !so->skipScan && !so->scanBehind && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 4e455a66b..3bad720ff 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -30,6 +30,17 @@
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									  int32 set_elem_result, Datum tupdatum, bool tupnull);
+static void _bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_array_set_low_or_high(Relation rel, ScanKey skey,
+									  BTArrayKeyInfo *array, bool low_not_high);
+static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -207,6 +218,7 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 	int32		result = 0;
 
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
+	Assert(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)));
 
 	if (tupnull)				/* NULL tupdatum */
 	{
@@ -283,6 +295,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* SAOP arrays never have NULLs */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -405,6 +419,185 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, since skip arrays don't really
+ * contain elements (they generate their array elements procedurally instead).
+ * Our interface matches that of _bt_binsrch_array_skey in every other way.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  We return -1 when tupdatum/tupnull is lower that any value
+ * within the range of the array, and 1 when it is higher than every value.
+ * Caller should pass *set_elem_result to _bt_skiparray_set_element to advance
+ * the array.
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (array->null_elem)
+	{
+		Assert(!array->low_compare && !array->high_compare);
+
+		*set_elem_result = 0;
+		return;
+	}
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+
+	/*
+	 * Assert that any keys that were assumed to be satisfied already (due to
+	 * caller passing cur_elem_trig=true) really are satisfied as expected
+	 */
+#ifdef USE_ASSERT_CHECKING
+	if (cur_elem_trig)
+	{
+		if (ScanDirectionIsForward(dir) && array->low_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												  array->low_compare->sk_collation,
+												  tupdatum,
+												  array->low_compare->sk_argument)));
+
+		if (ScanDirectionIsBackward(dir) && array->high_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												  array->high_compare->sk_collation,
+												  tupdatum,
+												  array->high_compare->sk_argument)));
+	}
+#endif
+}
+
+/*
+ * _bt_skiparray_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Caller passes set_elem_result returned by _bt_binsrch_skiparray_skey for
+ * caller's tupdatum/tupnull.
+ *
+ * We copy tupdatum/tupnull into skey's sk_argument iff set_elem_result == 0.
+ * Otherwise, we set skey to either the lowest or highest value that's within
+ * the range of caller's skip array (whichever is the best available match to
+ * tupdatum/tupnull that is still within the range of the skip array according
+ * to _bt_binsrch_skiparray_skey/set_elem_result).
+ */
+static void
+_bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  int32 set_elem_result, Datum tupdatum, bool tupnull)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (set_elem_result)
+	{
+		/* tupdatum/tupnull is out of the range of the skip array */
+		Assert(!array->null_elem);
+
+		_bt_array_set_low_or_high(rel, skey, array, set_elem_result < 0);
+		return;
+	}
+
+	/* Advance skip array to tupdatum (or tupnull) value */
+	if (unlikely(tupnull))
+	{
+		_bt_skiparray_set_isnull(rel, skey, array);
+		return;
+	}
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen);
+}
+
+/*
+ * _bt_skiparray_set_isnull() -- set skip array scan key to NULL
+ */
+static void
+_bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->null_elem && !array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -414,29 +607,355 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_array_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_array_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  bool low_not_high)
+{
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting array to lowest element (according to low_compare) */
+		skey->sk_flags |= SK_BT_MINVAL;
+	}
+	else
+	{
+		/* Setting array to highest element (according to high_compare) */
+		skey->sk_flags |= SK_BT_MAXVAL;
+	}
+}
+
+/*
+ * _bt_array_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the minimum value within the range
+	 * of a skip array (often just -inf) is never decrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		/* Successfully "decremented" array */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly decrement sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Decrement" from NULL to the high_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->high_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup->decrement(rel, skey->sk_argument, &uflow);
+	if (unlikely(uflow))
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			_bt_skiparray_set_isnull(rel, skey, array);
+
+			/* Successfully "decremented" array to NULL */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	/* Successfully decremented array */
+	return true;
+}
+
+/*
+ * _bt_array_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MINVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the maximum value within the range
+	 * of a skip array (often just +inf) is never incrementable
+	 */
+	if (skey->sk_flags & SK_BT_MAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		/* Successfully "incremented" array */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly increment sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Increment" from NULL to the low_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->low_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup->increment(rel, skey->sk_argument, &oflow);
+	if (unlikely(oflow))
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			_bt_skiparray_set_isnull(rel, skey, array);
+
+			/* Successfully "incremented" array to NULL */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	/* Successfully incremented array */
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -452,6 +971,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -461,29 +981,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_array_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_array_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -507,7 +1028,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 }
 
 /*
- * _bt_rewind_nonrequired_arrays() -- Rewind non-required arrays
+ * _bt_rewind_nonrequired_arrays() -- Rewind SAOP arrays not marked required
  *
  * Called when _bt_advance_array_keys decides to start a new primitive index
  * scan on the basis of the current scan position being before the position
@@ -539,10 +1060,15 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
  *
  * Note: _bt_verify_arrays_bt_first is called by an assertion to enforce that
  * everybody got this right.
+ *
+ * Note: In practice almost all SAOP arrays are marked required during
+ * preprocessing (if necessary by generating skip arrays).  It is hardly ever
+ * truly necessary to call here, but consistently doing so is simpler.
  */
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -550,7 +1076,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -562,16 +1087,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_array_set_low_or_high(rel, cur, array,
+								  ScanDirectionIsForward(dir));
 	}
 }
 
@@ -696,9 +1215,77 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (likely(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))))
+		{
+			/* Scankey has a valid/comparable sk_argument value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0)
+			{
+				/*
+				 * Interpret result in a way that takes NEXT/PRIOR into
+				 * account
+				 */
+				if (cur->sk_flags & SK_BT_NEXT)
+					result = -1;
+				else if (cur->sk_flags & SK_BT_PRIOR)
+					result = 1;
+
+				Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP));
+			}
+		}
+		else
+		{
+			BTArrayKeyInfo *array = NULL;
+
+			/*
+			 * Current array element/array = scan key value is a sentinel
+			 * value that represents the lowest (or highest) possible value
+			 * that's still within the range of the array.
+			 *
+			 * Like _bt_first, we only see MINVAL keys during forwards scans
+			 * (and similarly only see MAXVAL keys during backwards scans).
+			 * Even if the scan's direction changes, we'll stop at some higher
+			 * order key before we can ever reach any MAXVAL (or MINVAL) keys.
+			 * (However, unlike _bt_first we _can_ get to keys marked either
+			 * NEXT or PRIOR, regardless of the scan's current direction.)
+			 */
+			Assert(ScanDirectionIsForward(dir) ?
+				   !(cur->sk_flags & SK_BT_MAXVAL) :
+				   !(cur->sk_flags & SK_BT_MINVAL));
+
+			/*
+			 * There are no valid sk_argument values in MINVAL/MAXVAL keys.
+			 * Check if tupdatum is within the range of skip array instead.
+			 */
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			_bt_binsrch_skiparray_skey(false, dir, tupdatum, tupnull,
+									   array, cur, &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum satisfies both low_compare and high_compare, so
+				 * it's time to advance the array keys.
+				 *
+				 * Note: It's possible that the skip array will "advance" from
+				 * its MINVAL (or MAXVAL) representation to an alternative,
+				 * logically equivalent representation of the same value: a
+				 * representation where the = key gets a valid datum in its
+				 * sk_argument.  This is only possible when low_compare uses
+				 * the >= strategy (or high_compare uses the <= strategy).
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1017,18 +1604,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1053,18 +1631,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -1080,14 +1649,22 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			bool		cur_elem_trig = (sktrig_required && ikey == sktrig);
 
 			/*
-			 * Binary search for closest match that's available from the array
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems == -1)
+				_bt_binsrch_skiparray_skey(cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * Binary search for the closest match from the SAOP array
+			 */
+			else
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 		}
 		else
 		{
@@ -1163,11 +1740,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+		if (array)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->num_elems == -1)
+			{
+				/* Skip array's new element is tupdatum (or MINVAL/MAXVAL) */
+				_bt_skiparray_set_element(rel, cur, array, result,
+										  tupdatum, tupnull);
+			}
+			else if (array->cur_elem != set_elem)
+			{
+				/* SAOP array's new element is set_elem datum */
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
 		}
 	}
 
@@ -1586,10 +2173,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -1920,6 +2508,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key uses one of several sentinel values.  We just
+		 * fall back on _bt_tuple_before_array_skeys when we see such a value.
+		 */
+		if (key->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL |
+							 SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index dd6f5a15c..817ad358f 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -106,6 +106,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 8546366ee..a6dd8eab5 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("ordering equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (GetIndexAmRoutineByAmId(amoid, false)->amcanhash)
 	{
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index f279853de..4227ab1a7 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f23cfad71..244f48f4f 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 5b35debc8..bb46ed0c7 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -193,6 +193,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -214,6 +216,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5943,6 +5947,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -7001,6 +7091,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -7010,17 +7147,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
+	List	   *indexSkipQuals;
 	int			indexcol;
 	bool		eqQualHere;
-	bool		found_saop;
+	bool		found_row_compare;
+	bool		found_array;
 	bool		found_is_null_op;
+	bool		have_correlation = false;
 	double		num_sa_scans;
+	double		correlation = 0.0;
 	ListCell   *lc;
 
 	/*
@@ -7031,19 +7170,24 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * it's OK to count them in indexSelectivity, but they should not count
 	 * for estimating numIndexTuples.  So we must examine the given indexquals
 	 * to find out which ones count as boundary quals.  We rely on the
-	 * knowledge that they are given in index column order.
+	 * knowledge that they are given in index column order.  Note that nbtree
+	 * preprocessing can add skip arrays that act as leading '=' quals in the
+	 * absence of ordinary input '=' quals, so in practice _most_ input quals
+	 * are able to act as index bound quals (which we take into account here).
 	 *
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
+	 * If there's a SAOP or skip array in the quals, we'll actually perform up
+	 * to N index descents (not just one), but the underlying array key's
 	 * operator can be considered to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
+	indexSkipQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
-	found_saop = false;
+	found_row_compare = false;
+	found_array = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -7051,17 +7195,202 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
 		ListCell   *lc2;
 
-		if (indexcol != iclause->indexcol)
+		if (indexcol < iclause->indexcol)
 		{
-			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			double		num_sa_scans_prev_cols = num_sa_scans;
+
+			/*
+			 * Beginning of a new column's quals.
+			 *
+			 * Skip scans use skip arrays, which are ScalarArrayOp style
+			 * arrays that generate their elements procedurally and on demand.
+			 * Given a composite index on "(a, b)", and an SQL WHERE clause
+			 * "WHERE b = 42", a skip scan will effectively use an indexqual
+			 * "WHERE a = ANY('{every col a value}') AND b = 42".  (Obviously,
+			 * the array on "a" must also return "IS NULL" matches, since our
+			 * WHERE clause used no strict operator on "a").
+			 *
+			 * Here we consider how nbtree will backfill skip arrays for any
+			 * index columns that lacked an '=' qual.  This maintains our
+			 * num_sa_scans estimate, and determines if this new column (the
+			 * "iclause->indexcol" column, not the prior "indexcol" column)
+			 * can have its RestrictInfos/quals added to indexBoundQuals.
+			 *
+			 * We'll need to handle columns that have inequality quals, where
+			 * the skip array generates values from a range constrained by the
+			 * quals (not every possible value, never with IS NULL matching).
+			 * indexSkipQuals tracks the prior column's quals (that is, the
+			 * "indexcol" column's quals) to help us with this.
+			 */
+			if (found_row_compare)
+			{
+				/*
+				 * Skip arrays can't be added after a RowCompare input qual
+				 * due to limitations in nbtree
+				 */
+				break;
+			}
+			if (eqQualHere)
+			{
+				/*
+				 * Don't need to add a skip array for an indexcol that already
+				 * has an '=' qual/equality constraint
+				 */
+				indexcol++;
+				indexSkipQuals = NIL;
+			}
 			eqQualHere = false;
-			indexcol++;
+
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct;
+				bool		isdefault = true;
+
+				found_array = true;
+
+				/*
+				 * A skipped attribute's ndistinct forms the basis of our
+				 * estimate of the total number of "array elements" used by
+				 * its skip array at runtime.  Look that up first.
+				 */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+				if (indexcol == 0)
+				{
+					/*
+					 * Get an estimate of the leading column's correlation in
+					 * passing (avoids rereading variable stats below)
+					 */
+					if (HeapTupleIsValid(vardata.statsTuple))
+						correlation = btcost_correlation(index, &vardata);
+					have_correlation = true;
+				}
+
+				ReleaseVariableStats(vardata);
+
+				/*
+				 * If ndistinct is a default estimate, conservatively assume
+				 * that no skipping will happen at runtime
+				 */
+				if (isdefault)
+				{
+					num_sa_scans = num_sa_scans_prev_cols;
+					break;		/* done building indexBoundQuals */
+				}
+
+				/*
+				 * Apply indexcol's indexSkipQuals selectivity to ndistinct
+				 */
+				if (indexSkipQuals != NIL)
+				{
+					List	   *partialSkipQuals;
+					Selectivity ndistinctfrac;
+
+					/*
+					 * If the index is partial, AND the index predicate with
+					 * the index-bound quals to produce a more accurate idea
+					 * of the number of distinct values for prior indexcol
+					 */
+					partialSkipQuals = add_predicate_to_index_quals(index,
+																	indexSkipQuals);
+
+					ndistinctfrac = clauselist_selectivity(root, partialSkipQuals,
+														   index->rel->relid,
+														   JOIN_INNER,
+														   NULL);
+
+					/*
+					 * If ndistinctfrac is selective (on its own), the scan is
+					 * unlikely to benefit from repositioning itself using
+					 * later quals.  Do not allow iclause->indexcol's quals to
+					 * be added to indexBoundQuals (it would increase descent
+					 * costs, without lowering numIndexTuples costs by much).
+					 */
+					if (ndistinctfrac < DEFAULT_RANGE_INEQ_SEL)
+					{
+						num_sa_scans = num_sa_scans_prev_cols;
+						break;	/* done building indexBoundQuals */
+					}
+
+					/* Adjust ndistinct downward */
+					ndistinct = rint(ndistinct * ndistinctfrac);
+					ndistinct = Max(ndistinct, 1);
+				}
+
+				/*
+				 * When there's no inequality quals, account for the need to
+				 * find an initial value by counting -inf/+inf as a value.
+				 *
+				 * We don't charge anything extra for possible next/prior key
+				 * index probes, which are sometimes used to find the next
+				 * valid skip array element (ahead of using the located
+				 * element value to relocate the scan to the next position
+				 * that might contain matching tuples).  It seems hard to do
+				 * better here.  Use of the skip support infrastructure often
+				 * avoids most next/prior key probes.  But even when it can't,
+				 * there's a decent chance that most individual next/prior key
+				 * probes will locate a leaf page whose key space overlaps all
+				 * of the scan's keys (even the lower-order keys) -- which
+				 * also avoids the need for a separate, extra index descent.
+				 * Note also that these probes are much cheaper than non-probe
+				 * primitive index scans: they're reliably very selective.
+				 */
+				if (indexSkipQuals == NIL)
+					ndistinct += 1;
+
+				/*
+				 * Update num_sa_scans estimate by multiplying by ndistinct.
+				 *
+				 * We make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * expecting skipping to be helpful...
+				 */
+				num_sa_scans *= ndistinct;
+
+				/*
+				 * ...but back out of adding this latest group of 1 or more
+				 * skip arrays when num_sa_scans exceeds the total number of
+				 * index pages (revert to num_sa_scans from before indexcol).
+				 * This causes a sharp discontinuity in cost (as a function of
+				 * the indexcol's ndistinct), but that is representative of
+				 * actual runtime costs.
+				 *
+				 * Note that skipping is helpful when each primitive index
+				 * scan only manages to skip over 1 or 2 irrelevant leaf pages
+				 * on average.  Skip arrays bring savings in CPU costs due to
+				 * the scan not needing to evaluate indexquals against every
+				 * tuple, which can greatly exceed any savings in I/O costs.
+				 * This test is a test of whether num_sa_scans implies that
+				 * we're past the point where the ability to skip ceases to
+				 * lower the scan's costs (even qual evaluation CPU costs).
+				 */
+				if (index->pages < num_sa_scans)
+				{
+					num_sa_scans = num_sa_scans_prev_cols;
+					break;		/* done building indexBoundQuals */
+				}
+
+				indexcol++;
+				indexSkipQuals = NIL;
+			}
+
+			/*
+			 * Finished considering the need to add skip arrays to bridge an
+			 * initial eqQualHere gap between the old and new index columns
+			 * (or there was no initial eqQualHere gap in the first place).
+			 *
+			 * If an initial gap could not be bridged, then new column's quals
+			 * (i.e. iclause->indexcol's quals) won't go into indexBoundQuals,
+			 * and so won't affect our final numIndexTuples estimate.
+			 */
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+				break;			/* done building indexBoundQuals */
 		}
 
+		Assert(indexcol == iclause->indexcol);
+
 		/* Examine each indexqual associated with this index clause */
 		foreach(lc2, iclause->indexquals)
 		{
@@ -7081,6 +7410,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_row_compare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -7089,7 +7419,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				double		alength = estimate_array_length(root, other_operand);
 
 				clause_op = saop->opno;
-				found_saop = true;
+				found_array = true;
 				/* estimate SA descents by indexBoundQuals only */
 				if (alength > 1)
 					num_sa_scans *= alength;
@@ -7101,7 +7431,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skip scan purposes */
 					eqQualHere = true;
 				}
 			}
@@ -7120,19 +7450,28 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
+
+			/*
+			 * We apply inequality selectivities to estimate index descent
+			 * costs with scans that use skip arrays.  Save this indexcol's
+			 * RestrictInfos if it looks like they'll be needed for that.
+			 */
+			if (!eqQualHere && !found_row_compare &&
+				indexcol < index->nkeycolumns - 1)
+				indexSkipQuals = lappend(indexSkipQuals, rinfo);
 		}
 	}
 
 	/*
 	 * If index is unique and we found an '=' clause for each column, we can
 	 * just assume numIndexTuples = 1 and skip the expensive
-	 * clauselist_selectivity calculations.  However, a ScalarArrayOp or
-	 * NullTest invalidates that theory, even though it sets eqQualHere.
+	 * clauselist_selectivity calculations.  However, an array or NullTest
+	 * always invalidates that theory (even when eqQualHere has been set).
 	 */
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
-		!found_saop &&
+		!found_array &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
 	else
@@ -7154,7 +7493,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		numIndexTuples = btreeSelectivity * index->rel->tuples;
 
 		/*
-		 * btree automatically combines individual ScalarArrayOpExpr primitive
+		 * btree automatically combines individual array element primitive
 		 * index scans whenever the tuples covered by the next set of array
 		 * keys are close to tuples covered by the current set.  That puts a
 		 * natural ceiling on the worst case number of descents -- there
@@ -7172,16 +7511,18 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * of leaf pages (we make it 1/3 the total number of pages instead) to
 		 * give the btree code credit for its ability to continue on the leaf
 		 * level with low selectivity scans.
+		 *
+		 * Note: num_sa_scans includes both ScalarArrayOp array elements and
+		 * skip array elements whose qual affects our numIndexTuples estimate.
 		 */
 		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
-		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
-		 * to integer.
+		 * As in genericcostestimate(), we have to adjust for any array quals
+		 * included in indexBoundQuals, and then round to integer.
 		 *
-		 * It is tempting to make genericcostestimate behave as if SAOP
+		 * It is tempting to make genericcostestimate behave as if array
 		 * clauses work in almost the same way as scalar operators during
 		 * btree scans, making the top-level scan look like a continuous scan
 		 * (as opposed to num_sa_scans-many primitive index scans).  After
@@ -7214,7 +7555,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * comparisons to descend a btree of N leaf tuples.  We charge one
 	 * cpu_operator_cost per comparison.
 	 *
-	 * If there are ScalarArrayOpExprs, charge this once per estimated SA
+	 * If there are SAOP/skip array keys, charge this once per estimated SA
 	 * index descent.  The ones after the first one are not startup cost so
 	 * far as the overall plan goes, so just add them to "total" cost.
 	 */
@@ -7234,110 +7575,25 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * cost is somewhat arbitrarily set at 50x cpu_operator_cost per page
 	 * touched.  The number of such pages is btree tree height plus one (ie,
 	 * we charge for the leaf page too).  As above, charge once per estimated
-	 * SA index descent.
+	 * SAOP/skip array descent.
 	 */
 	descentCost = (index->tree_height + 1) * DEFAULT_PAGE_CPU_MULTIPLIER * cpu_operator_cost;
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* btcost_correlation already called earlier on */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..2bd35d2d2
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns skip support struct, allocating in caller's memory
+ * context.  Otherwise returns NULL, indicating that operator class has no
+ * skip support function.
+ */
+SkipSupport
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse)
+{
+	Oid			skipSupportFunction;
+	SkipSupport sksup;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return NULL;
+
+	sksup = palloc(sizeof(SkipSupportData));
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return sksup;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 9682f9dbd..347089b76 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2304,6 +2305,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 4f8402ef9..1b72059d2 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,6 +13,7 @@
 
 #include "postgres.h"
 
+#include <limits.h>
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
@@ -21,6 +22,7 @@
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -416,6 +418,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..3e6f30d74 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value that
+     might be stored in an index, so the domain of the particular data type
+     stored within the index (the input opclass type) must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index 768b77aa0..d5adb58c1 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -835,7 +835,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index aaa6586d3..e6a0b7093 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4249,7 +4249,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage
      <replaceable>column_name</replaceable> =
      <replaceable>value2</replaceable> ...</literal> construct, though only
     when the optimizer transforms the construct into an equivalent
-    multi-valued array representation.
+    multi-valued array representation.  Similarly, when B-Tree index scans use
+    the skip scan strategy, an index search is performed each time the scan is
+    repositioned to the next index leaf page that might have matching tuples.
    </para>
   </note>
   <tip>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index 387baac7e..d0470ac79 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -860,6 +860,37 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE thousand IN (1, 2, 3, 4);
     <structname>tenk1_thous_tenthous</structname> index leaf page.
    </para>
 
+   <para>
+    The <quote>Index Searches</quote> line is also useful with B-tree index
+    scans that apply the <firstterm>skip scan</firstterm> optimization to
+    more efficiently traverse through an index:
+<screen>
+EXPLAIN ANALYZE SELECT four, unique1 FROM tenk1 WHERE four BETWEEN 1 AND 3 AND unique1 = 42;
+                                                              QUERY PLAN
+-------------------------------------------------------------------&zwsp;---------------------------------------------------------------
+ Index Only Scan using tenk1_four_unique1_idx on tenk1  (cost=0.29..6.90 rows=1 width=8) (actual time=0.006..0.007 rows=1.00 loops=1)
+   Index Cond: ((four &gt;= 1) AND (four &lt;= 3) AND (unique1 = 42))
+   Heap Fetches: 0
+   Index Searches: 3
+   Buffers: shared hit=7
+ Planning Time: 0.029 ms
+ Execution Time: 0.012 ms
+</screen>
+
+    Here we see an Index-Only Scan node using
+    <structname>tenk1_four_unique1_idx</structname>, a composite index on the
+    <structname>tenk1</structname> table's <structfield>four</structfield> and
+    <structfield>unique1</structfield> columns.  The scan performs 3 searches
+    that each read a single index leaf page:
+    <quote><literal>four = 1 AND unique1 = 42</literal></quote>,
+    <quote><literal>four = 2 AND unique1 = 42</literal></quote>, and
+    <quote><literal>four = 3 AND unique1 = 42</literal></quote>.  This index
+    is generally a good target for skip scan, since its leading column (the
+    <structfield>four</structfield> column) contains only 4 distinct values,
+    while its second/final column (the <structfield>unique1</structfield>
+    column) contains many distinct values.
+   </para>
+
    <para>
     Another type of extra information is the number of rows removed by a
     filter condition:
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 053619624..7e23a7b6e 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index 0c274d56a..23bf33f10 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  ordering equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 8879554c3..bfb1a286e 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -581,6 +581,47 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Index Only Scan using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Index Only Scan Backward using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 --
 -- Test for multilevel page deletion
 --
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index bd5f002cf..15dde752f 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1637,7 +1637,9 @@ DROP TABLE syscol_table;
 -- Tests for IS NULL/IS NOT NULL with b-tree indexes
 --
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 SET enable_seqscan = OFF;
 SET enable_indexscan = ON;
@@ -1645,7 +1647,7 @@ SET enable_bitmapscan = ON;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1657,13 +1659,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1678,12 +1680,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1695,13 +1703,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1722,12 +1730,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0,
      1
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc nulls last,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1739,13 +1753,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1760,12 +1774,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2  nulls first,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1777,13 +1797,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1798,6 +1818,12 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 -- Check initial-positioning logic too
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2);
@@ -1829,20 +1855,24 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
 (2 rows)
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-         |        
-     278 |     999
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 5;
+ unique1 |  unique2   
+---------+------------
+     500 |           
+     100 |           
+         |           
+         | 2147483647
+     278 |        999
+(5 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-     278 |     999
-       0 |     998
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 3;
+ unique1 |  unique2   
+---------+------------
+         | 2147483647
+     278 |        999
+       0 |        998
+(3 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
@@ -2247,7 +2277,8 @@ SELECT count(*) FROM dupindexcols
 (1 row)
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 explain (costs off)
 SELECT unique1 FROM tenk1
@@ -2269,7 +2300,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2289,7 +2320,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2309,6 +2340,25 @@ ORDER BY thousand DESC, tenthous DESC;
         0 |     3000
 (2 rows)
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ Index Only Scan Backward using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > 995) AND (tenthous = ANY ('{998,999}'::integer[])))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+ thousand | tenthous 
+----------+----------
+      999 |      999
+      998 |      998
+(2 rows)
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -2339,6 +2389,45 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 ---------
 (0 rows)
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+                   QUERY PLAN                    
+-------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY (NULL::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY ('{NULL,NULL,NULL}'::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: ((unique1 IS NULL) AND (unique1 IS NULL))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+ unique1 
+---------
+(0 rows)
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
                                 QUERY PLAN                                 
@@ -2462,6 +2551,44 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 ---------
 (0 rows)
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > '-1'::integer) AND (thousand >= 0) AND (tenthous = 3000))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+(1 row)
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand < 3) AND (thousand <= 2) AND (tenthous = 1001))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        1 |     1001
+(1 row)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 8687ffe27..2c718e03f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5332,9 +5332,10 @@ Function              | in_range(time without time zone,time without time zone,i
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql
index 670ad5c6e..68c61dbc7 100644
--- a/src/test/regress/sql/btree_index.sql
+++ b/src/test/regress/sql/btree_index.sql
@@ -327,6 +327,27 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 
 --
 -- Test for multilevel page deletion
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index be570da08..40ba3d65b 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -650,7 +650,9 @@ DROP TABLE syscol_table;
 --
 
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 
 SET enable_seqscan = OFF;
@@ -663,6 +665,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -675,6 +678,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NUL
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0, 1);
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -686,6 +690,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -697,6 +702,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -716,9 +722,9 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
   ORDER BY unique2 LIMIT 2;
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 5;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 3;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
 
@@ -852,7 +858,8 @@ SELECT count(*) FROM dupindexcols
   WHERE f1 BETWEEN 'WA' AND 'ZZZ' and id < 1000 and f1 ~<~ 'YX';
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 
 explain (costs off)
@@ -864,7 +871,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -874,7 +881,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -884,6 +891,15 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand DESC, tenthous DESC;
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -897,6 +913,21 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 
 SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('{33, 44}'::bigint[]);
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
 
@@ -942,6 +973,26 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
 SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index bfa276d2d..977a3cebc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -223,6 +223,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2736,6 +2737,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.47.2

v29-0003-Lower-nbtree-skip-array-maintenance-overhead.patchapplication/octet-stream; name=v29-0003-Lower-nbtree-skip-array-maintenance-overhead.patchDownload
From 4bc77659b810af886b156e19391c6c31c92db4b3 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v29 3/5] Lower nbtree skip array maintenance overhead.

Add an optimization that fixes regressions in index scans that are
nominally eligible to use skip scan, but can never actually benefit from
skipping.  These are cases where a leading prefix column contains many
distinct values -- especially when the number of values approaches the
total number of index tuples, where skipping can never be profitable.

The optimization is activated dynamically, as a fallback strategy.  It
works by determining a prefix of leading index columns whose scan keys
(often skip array scan keys) are guaranteed to be satisfied by every
possible index tuple on a given page.  _bt_readpage is then able to
start comparisons at the first scan key that might not be satisfied.
This necessitates making _bt_readpage temporarily cease maintaining the
scan's arrays.  _bt_checkkeys will treat the scan's keys as if they were
not marked as required during preprocessing.  This process relies on the
non-required SAOP array logic in _bt_advance_array_keys that was added
to Postgres 17 by commit 5bf748b8.

The new optimization does not affect array primitive scan scheduling.
It is similar to the precheck optimization added by Postgres 17 commit
e0b1ee17dc, though it is only used during nbtree scans with skip arrays.
It can be applied during scans that were never eligible for the precheck
optimization.  As a result, many scans that cannot benefit from skipping
will still benefit from using skip arrays (skip arrays indirectly enable
the use of the optimization introduced by this commit).

Skip scan's approach of adding skip arrays during preprocessing and then
fixing (or significantly ameliorating) the resulting regressions seen in
unsympathetic cases is enabled by the optimization added by this commit
(and by the "look ahead" optimization introduced by commit 5bf748b8).
This allows the planner to avoid generating distinct, competing index
paths (one path for skip scan, another for an equivalent traditional
full index scan).  The overall effect is to make scan runtime close to
optimal, even when the planner works off an incorrect cardinality
estimate.  Scans will also perform well given a skipped column with data
skew: individual groups of pages with many distinct values in respect of
a skipped column can be read about as efficiently as before, without
having to give up on skipping over other provably-irrelevant leaf pages.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h           |   5 +-
 src/backend/access/nbtree/nbtsearch.c |  39 ++-
 src/backend/access/nbtree/nbtutils.c  | 431 +++++++++++++++++++++++---
 3 files changed, 429 insertions(+), 46 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index e4296c9c5..df653d8b2 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1115,11 +1115,13 @@ typedef struct BTReadPageState
 
 	/*
 	 * Input and output parameters, set and unset by both _bt_readpage and
-	 * _bt_checkkeys to manage precheck optimizations
+	 * _bt_checkkeys to manage precheck and forcenonrequired optimizations
 	 */
 	bool		firstpage;		/* on first page of current primitive scan? */
 	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
+	bool		forcenonrequired;	/* treat all scan keys as nonrequired? */
+	int			ikey;			/* start comparisons from ikey'th scan key */
 
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
@@ -1328,6 +1330,7 @@ extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arra
 						  IndexTuple tuple, int tupnatts);
 extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
 									 IndexTuple finaltup);
+extern void _bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index b4f72d82d..f0e23441e 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1651,6 +1651,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.firstpage = firstpage;
 	pstate.prechecked = false;
 	pstate.firstmatch = false;
+	pstate.forcenonrequired = false;
+	pstate.ikey = 0;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
@@ -1670,7 +1672,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * We don't do this for the first page read by each (primitive) scan, to
 	 * avoid slowing down point queries.  They typically don't stand to gain
 	 * much when the optimization can be applied, and are more likely to
-	 * notice the overhead of the precheck.  Also avoid it during skip scans.
+	 * notice the overhead of the precheck.  Also avoid it during skip scans,
+	 * where we prefer to apply the similar _bt_skip_ikeyprefix optimization.
 	 *
 	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
 	 * just set a low-order required array's key to the best available match
@@ -1734,6 +1737,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			}
 
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
+
+			/*
+			 * Use pstate.ikey optimization during primitive index scans with
+			 * skip arrays once we've already read its first page
+			 */
+			if (!pstate.firstpage && so->skipScan && minoff < maxoff)
+				_bt_skip_ikeyprefix(scan, &pstate);
 		}
 
 		/* load items[] in ascending order */
@@ -1772,6 +1782,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum < pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1835,6 +1846,15 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			IndexTuple	itup = (IndexTuple) PageGetItem(page, iid);
 			int			truncatt;
 
+			if (pstate.forcenonrequired)
+			{
+				Assert(so->skipScan);
+
+				/* stop treating required keys as nonrequired */
+				pstate.forcenonrequired = false;
+				pstate.firstmatch = false;
+				pstate.ikey = 0;
+			}
 			truncatt = BTreeTupleGetNAtts(itup, rel);
 			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
@@ -1874,6 +1894,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			}
 
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
+
+			/*
+			 * Use pstate.ikey optimization during primitive index scans with
+			 * skip arrays once we've already read its first page
+			 */
+			if (!pstate.firstpage && so->skipScan && minoff < maxoff)
+				_bt_skip_ikeyprefix(scan, &pstate);
 		}
 
 		/* load items[] in descending order */
@@ -1915,6 +1942,15 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff && pstate.forcenonrequired)
+			{
+				Assert(so->skipScan);
+
+				/* stop treating required keys as nonrequired */
+				pstate.forcenonrequired = false;
+				pstate.firstmatch = false;
+				pstate.ikey = 0;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
@@ -1926,6 +1962,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum > pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 3bad720ff..5d75a7f29 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -57,11 +57,12 @@ static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool forcenonrequired,
+							  bool prechecked, bool firstmatch,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-								 ScanDirection dir, bool *continuescan);
+								 ScanDirection dir, bool forcenonrequired, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -1421,9 +1422,10 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
  * postcondition's <= operator with a >=.  In other words, just swap the
  * precondition with the postcondition.)
  *
- * We also deal with "advancing" non-required arrays here.  Callers whose
- * sktrig scan key is non-required specify sktrig_required=false.  These calls
- * are the only exception to the general rule about always advancing the
+ * We also deal with "advancing" non-required arrays here (or arrays that are
+ * treated as non-required for the duration of a _bt_readpage call).  Callers
+ * whose sktrig scan key is non-required specify sktrig_required=false.  These
+ * calls are the only exception to the general rule about always advancing the
  * required array keys (the scan may not even have a required array).  These
  * callers should just pass a NULL pstate (since there is never any question
  * of stopping the scan).  No call to _bt_tuple_before_array_skeys is required
@@ -1480,8 +1482,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		pstate->firstmatch = false;
 
-		/* Shouldn't have to invalidate 'prechecked', though */
-		Assert(!pstate->prechecked);
+		/* Shouldn't have to invalidate precheck/forcenonrequired state */
+		Assert(!pstate->prechecked && !pstate->forcenonrequired &&
+			   pstate->ikey == 0);
 
 		/*
 		 * Once we return we'll have a new set of required array keys, so
@@ -1490,6 +1493,26 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		pstate->rechecks = 0;
 		pstate->targetdistance = 0;
 	}
+	else if (sktrig < so->numberOfKeys - 1 &&
+			 !(so->keyData[so->numberOfKeys - 1].sk_flags & SK_SEARCHARRAY))
+	{
+		int			least_sign_ikey = so->numberOfKeys - 1;
+		bool		continuescan;
+
+		/*
+		 * Optimization: perform a precheck of the least significant key
+		 * during !sktrig_required calls when it isn't already our sktrig
+		 * (provided the precheck key is not itself an array).
+		 *
+		 * When the precheck works out we'll avoid an expensive binary search
+		 * of sktrig's array (plus any other arrays before least_sign_ikey).
+		 */
+		Assert(so->keyData[sktrig].sk_flags & SK_SEARCHARRAY);
+		if (!_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
+							   false, false, false, false,
+							   &continuescan, &least_sign_ikey))
+			return false;
+	}
 
 	Assert(_bt_verify_keys_with_arraykeys(scan));
 
@@ -1533,8 +1556,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -1668,7 +1689,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -1710,7 +1731,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -1725,7 +1746,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -1785,6 +1806,12 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * of any required scan key).  All that matters is whether caller's tuple
 	 * satisfies the new qual, so it's safe to just skip the _bt_check_compare
 	 * recheck when we've already determined that it can only return 'false'.
+	 *
+	 * Note: In practice most scan keys are marked required by preprocessing,
+	 * if necessary by generating a preceding skip array.  We nevertheless
+	 * often handle array keys marked required as if they were nonrequired.
+	 * This behavior is requested by our _bt_check_compare caller, though only
+	 * when it is passed "forcenonrequired=true" by _bt_checkkeys.
 	 */
 	if ((sktrig_required && all_required_satisfied) ||
 		(!sktrig_required && all_satisfied))
@@ -1796,7 +1823,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -2040,20 +2067,23 @@ new_prim_scan:
 	 * the scan arrives on the next sibling leaf page) when it has already
 	 * read at least one leaf page before the one we're reading now.  This is
 	 * important when reading subsets of an index with many distinct values in
-	 * respect of an attribute constrained by an array.  It encourages fewer,
-	 * larger primitive scans where that makes sense.
-	 *
-	 * Note: so->scanBehind is primarily used to indicate that the scan
-	 * encountered a finaltup that "satisfied" one or more required scan keys
-	 * on a truncated attribute value/-inf value.  We can safely reuse it to
-	 * force the scan to stay on the leaf level because the considerations are
-	 * exactly the same.
+	 * respect of an attribute constrained by an array (often a skip array).
+	 * It encourages fewer, larger primitive scans where that makes sense.
+	 * This will in turn encourage _bt_readpage to apply the forcenonrequired
+	 * optimization when applicable (i.e. when the scan has a skip array).
 	 *
 	 * Note: This heuristic isn't as aggressive as you might think.  We're
 	 * conservative about allowing a primitive scan to step from the first
 	 * leaf page it reads to the page's sibling page (we only allow it on
 	 * first pages whose finaltup strongly suggests that it'll work out).
 	 * Clearing this first page finaltup hurdle is a strong signal in itself.
+	 *
+	 * Note: so->scanBehind is primarily used to indicate that the scan
+	 * encountered a finaltup that "satisfied" one or more required scan keys
+	 * on a truncated attribute value/-inf value.  We reuse it to force the
+	 * scan to stay on the leaf level because the considerations are just the
+	 * same (the array's are ahead of the index key space, or they're behind
+	 * when we're scanning backwards).
 	 */
 	if (!pstate->firstpage)
 	{
@@ -2229,14 +2259,16 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	TupleDesc	tupdesc = RelationGetDescr(scan->indexRelation);
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ScanDirection dir = so->currPos.dir;
-	int			ikey = 0;
+	int			ikey = pstate->ikey;
 	bool		res;
 
+	Assert(ikey == 0 || pstate->forcenonrequired);
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
 
 	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+							arrayKeys, pstate->forcenonrequired,
+							pstate->prechecked, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
@@ -2247,12 +2279,12 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 *
 		 * Assert that the scan isn't in danger of becoming confused.
 		 */
-		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
+		Assert(!pstate->prechecked && !pstate->firstmatch &&
+			   !pstate->forcenonrequired);
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 	}
-	if (pstate->prechecked || pstate->firstmatch)
+	if ((pstate->prechecked || pstate->firstmatch) && !pstate->forcenonrequired)
 	{
 		bool		dcontinuescan;
 		int			dikey = 0;
@@ -2262,7 +2294,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 		 * get the same answer without those optimizations
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
+										false, false, false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
 	}
@@ -2285,6 +2317,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * It's also possible that the scan is still _before_ the _start_ of
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
+	Assert(!pstate->forcenonrequired);
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
@@ -2400,7 +2433,7 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	Assert(so->numArrayKeys);
 
 	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+					  false, false, false, false, &continuescan, &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -2408,6 +2441,270 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	return true;
 }
 
+/*
+ * Determine a prefix of scan keys that are guaranteed to be satisfied by
+ * every possible tuple on pstate's page.  Used during scans with skip arrays,
+ * which do not use the similar optimization controlled by pstate.prechecked.
+ *
+ * Sets pstate.ikey (and pstate.forcenonrequired) on success, making later
+ * calls to _bt_checkkeys start checks of each tuple from the so->keyData[]
+ * entry at pstate.ikey (while treating keys >= pstate.ikey as nonrequired).
+ *
+ * Caller must reset pstate.ikey and pstate.forcenonrequired just ahead of the
+ * _bt_checkkeys call for the page's final tuple (the pstate.finaltup tuple).
+ * That will give _bt_checkkeys the opportunity to call _bt_advance_array_keys
+ * properly (with sktrig_required=true) should the array keys need to advance
+ * on this page (which is likely but not a certainty).  Caller doesn't need to
+ * do this on the rightmost/leftmost page in the index (where pstate.finaltup
+ * won't ever be set).
+ */
+void
+_bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	ScanDirection dir = so->currPos.dir;
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	ItemId		iid;
+	IndexTuple	firsttup,
+				lasttup;
+	int			ikey = 0,
+				arrayidx = 0,
+				firstchangingattnum;
+
+	Assert(so->skipScan && pstate->minoff < pstate->maxoff);
+
+	/* minoff is an offset to the lowest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->minoff);
+	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* maxoff is an offset to the highest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->maxoff);
+	lasttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* Determine the first attribute whose values change on caller's page */
+	firstchangingattnum = _bt_keep_natts_fast(rel, firsttup, lasttup);
+
+	for (; ikey < so->numberOfKeys; ikey++)
+	{
+		ScanKey		key = so->keyData + ikey;
+		BTArrayKeyInfo *array;
+		Datum		tupdatum;
+		bool		tupnull;
+		int32		result;
+
+		/*
+		 * Determine if it's safe to set pstate.ikey to an offset to a key
+		 * that comes after this key, by examining this key
+		 */
+		if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) == 0)
+		{
+			/*
+			 * This is the first key that is not marked required (only happens
+			 * when _bt_preprocess_keys couldn't mark all keys required due to
+			 * implementation restrictions affecting skip array generation)
+			 */
+			Assert(!(key->sk_flags & SK_BT_SKIP));
+			break;				/* pstate.ikey to be set to nonrequired ikey */
+		}
+		if (key->sk_flags & SK_ROW_HEADER)
+		{
+			/* Don't support skipping over RowCompare inequalities */
+			break;				/* pstate.ikey to be set to RowCompare's ikey */
+		}
+		if (key->sk_strategy != BTEqualStrategyNumber)
+		{
+			/*
+			 * This is the scan's first inequality key (barring inequalities
+			 * that are used to describe the range of a = skip array key).
+			 *
+			 * It's definitely safe for _bt_checkkeys to avoid assessing this
+			 * inequality when the page's first and last non-pivot tuples both
+			 * satisfy the inequality (since the same must also be true of all
+			 * the tuples in between these two).
+			 *
+			 * Unlike the "=" case, it doesn't matter if this attribute has
+			 * more than one distinct value (though it is necessary for any
+			 * and all _prior_ attributes to contain no more than one distinct
+			 * value amongst all of the tuples from pstate.page).
+			 */
+			if (key->sk_attno > firstchangingattnum)
+				break;			/* pstate.ikey to be set to inequality's ikey */
+
+			tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &tupnull);
+			if (tupnull)
+				break;			/* pstate.ikey to be set to inequality's ikey */
+			if (!DatumGetBool(FunctionCall2Coll(&key->sk_func,
+												key->sk_collation, tupdatum,
+												key->sk_argument)))
+				break;
+
+			tupdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &tupnull);
+			if (tupnull)
+				break;			/* pstate.ikey to be set to inequality's ikey */
+
+			if (!DatumGetBool(FunctionCall2Coll(&key->sk_func,
+												key->sk_collation, tupdatum,
+												key->sk_argument)))
+				break;			/* pstate.ikey to be set to inequality's ikey */
+
+			continue;			/* safe, key satisfied by every tuple */
+		}
+		if (!(key->sk_flags & SK_SEARCHARRAY))
+		{
+			/*
+			 * Found a scalar (non-array) = key.
+			 *
+			 * It is unsafe to set pstate.ikey to an ikey beyond this key,
+			 * unless the = key is satisfied by every possible tuple on the
+			 * page (possible only when attribute has just one distinct value
+			 * among all tuples on the page).
+			 */
+			if (key->sk_attno < firstchangingattnum)
+			{
+				/*
+				 * Only one distinct value on the page for this key's column.
+				 * Must still make sure that = key is actually satisfied by
+				 * the value that is stored within every tuple on the page.
+				 */
+				tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+										 &tupnull);
+				result = _bt_compare_array_skey(&so->orderProcs[ikey],
+												tupdatum, tupnull,
+												key->sk_argument, key);
+				if (result == 0)
+					continue;	/* safe, = key satisfied by every tuple */
+			}
+			break;				/* pstate.ikey to be set to scalar key's ikey */
+		}
+
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == ikey);
+		if (array->num_elems != -1)
+		{
+			/*
+			 * Found a SAOP array = key.
+			 *
+			 * Handle this just like we handle scalar = keys.
+			 */
+			if (key->sk_attno < firstchangingattnum)
+			{
+				/*
+				 * Only one distinct value on the page for this key's column.
+				 * Must still make sure that SAOP array is actually satisfied
+				 * by the value that is stored within every tuple on the page.
+				 */
+				tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+										 &tupnull);
+				_bt_binsrch_array_skey(&so->orderProcs[ikey], false,
+									   NoMovementScanDirection,
+									   tupdatum, tupnull, array, key, &result);
+				if (result == 0)
+					continue;	/* safe, SAOP = key satisfied by every tuple */
+			}
+			break;				/* pstate.ikey to be set to SAOP array's ikey */
+		}
+
+		/*
+		 * Found a skip array = key.
+		 *
+		 * As with other = keys, moving past this skip array key is safe when
+		 * every tuple on the page is guaranteed to satisfy the array's key.
+		 * But we can be more aggressive here, since skip arrays make it easy
+		 * to assess whether all the values on the page fall within the skip
+		 * array's entire range.
+		 */
+		if (array->null_elem)
+		{
+			/* Safe, non-range skip array "satisfied" by every tuple on page */
+			continue;
+		}
+		else if (key->sk_attno > firstchangingattnum)
+		{
+			/*
+			 * We cannot assess whether this range skip array will definitely
+			 * be satisfied by every tuple on the page, since its attribute is
+			 * preceded by another attribute that is not certain to contain
+			 * the same prefix of value(s) within every tuple from pstate.page
+			 */
+			break;				/* pstate.ikey to be set to range array's ikey */
+		}
+
+		/*
+		 * Found a range skip array = key.
+		 *
+		 * This is just like the inequality case (though the inequality keys
+		 * tested here are in the array itself, not in so->keyData[]).
+		 */
+		tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   tupdatum, tupnull, array, key, &result);
+		if (result != 0)
+			break;				/* pstate.ikey to be set to range array's ikey */
+
+		tupdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   tupdatum, tupnull, array, key, &result);
+		if (result != 0)
+			break;				/* pstate.ikey to be set to range array's ikey */
+
+		/* Safe, range skip array satisfied by every tuple on page */
+	}
+
+	/*
+	 * If pstate.ikey remains 0, _bt_advance_array_keys will still be able to
+	 * apply its precheck optimization when dealing with "nonrequired" array
+	 * keys.  That's reason enough on its own to set forcenonrequired=true.
+	 */
+	pstate->forcenonrequired = true;	/* do this unconditionally */
+	pstate->ikey = ikey;
+
+	/*
+	 * Set the element for range skip arrays whose ikey is >= pstate.ikey to
+	 * whatever the first array element is in the scan's current direction.
+	 * This allows range skip arrays that will never be satisfied by any tuple
+	 * on the page to avoid extra sk_argument comparisons -- _bt_check_compare
+	 * won't use the key's sk_argument when the key is marked MINVAL/MAXVAL
+	 * (note that MINVAL/MAXVAL won't be unset until an exact match is found,
+	 * which might not happen for any tuple on the page).
+	 *
+	 * Set the element for non-range skip arrays whose ikey is >= pstate.ikey
+	 * to NULL (regardless of whether NULLs are stored first or last), too.
+	 * This allows non-range skip arrays (recognized by _bt_check_compare as
+	 * "non-required" skip arrays with ISNULL set) to avoid needlessly calling
+	 * _bt_advance_array_keys (it isn't necessary because we know that any
+	 * non-range skip array must be satisfied by every possible value).
+	 *
+	 * Note: It's safe for us to set ISNULL like this without regard for
+	 * whether it'll leave the array keys before or after the page's keyspace.
+	 * We know that once caller unsets pstate.forcenonrequired, its call to
+	 * _bt_checkkeys will still be able to advance the scan's array keys,
+	 * without hindrance from _bt_tuple_before_array_skeys.  We also know that
+	 * a sktrig_required=false call to _bt_advance_array_keys won't alter any
+	 * array unless an exact match is available to "advance" the array to.
+	 */
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		key = &so->keyData[array->scan_key];
+
+		Assert(key->sk_flags & SK_SEARCHARRAY);
+
+		if (array->num_elems != -1)
+			continue;
+		if (array->scan_key < pstate->ikey)
+			continue;
+
+		Assert(key->sk_flags & SK_BT_SKIP);
+
+		if (!array->null_elem)
+			_bt_array_set_low_or_high(rel, key, array,
+									  ScanDirectionIsForward(dir));
+		else
+			_bt_skiparray_set_isnull(rel, key, array);
+	}
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
@@ -2439,17 +2736,25 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Though we advance non-required array keys on our own, that shouldn't have
  * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
+ * have no fixed relationship with the scan's progress.
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass forcenonrequired=true to instruct us to treat all keys as nonrequried.
+ * This is used to make it safe to temporarily stop properly maintaining the
+ * scan's required arrays.  Callers can determine which prefix of keys must
+ * satisfy every possible prefix of index attribute values on the page, and
+ * then pass us an initial *ikey for the first key that might be unsatisified.
+ * We won't be maintaining any arrays before that initial *ikey, so there is
+ * no point in trying to do so for any later arrays.  (Callers that do this
+ * must be careful to reset the array keys when they finish reading the page.)
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool forcenonrequired,
+				  bool prechecked, bool firstmatch,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -2466,10 +2771,13 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when we're forced to treat all scan keys as nonrequired)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (forcenonrequired)
+			Assert(!prechecked);
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
@@ -2517,6 +2825,19 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		{
 			Assert(key->sk_flags & SK_SEARCHARRAY);
 			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir || forcenonrequired);
+
+			/*
+			 * Cannot fall back on _bt_tuple_before_array_skeys when we're
+			 * treating the scan's keys as nonrequired, though.  Just handle
+			 * this like any other non-required equality-type array key.
+			 */
+			if (forcenonrequired)
+			{
+				Assert(!(key->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+				return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
+											  tupdesc, *ikey, false);
+			}
 
 			*continuescan = false;
 			return false;
@@ -2526,7 +2847,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
-									 continuescan))
+									 forcenonrequired, continuescan))
 				continue;
 			return false;
 		}
@@ -2558,9 +2879,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			 */
 			if (requiredSameDir)
 				*continuescan = false;
+			else if (unlikely(key->sk_flags & SK_BT_SKIP))
+			{
+				/*
+				 * If we're treating scan keys as nonrequired, and encounter a
+				 * skip array scan key whose current element is NULL, then it
+				 * must be a non-range skip array.  It must be satisfied, so
+				 * there's no need to call _bt_advance_array_keys to check.
+				 */
+				Assert(forcenonrequired && *ikey > 0);
+				continue;
+			}
 
 			/*
-			 * In any case, this indextuple doesn't match the qual.
+			 * This indextuple doesn't match the qual.
 			 */
 			return false;
 		}
@@ -2581,7 +2913,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -2599,7 +2931,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -2668,7 +3000,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
  */
 static bool
 _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
-					 TupleDesc tupdesc, ScanDirection dir, bool *continuescan)
+					 TupleDesc tupdesc, ScanDirection dir,
+					 bool forcenonrequired, bool *continuescan)
 {
 	ScanKey		subkey = (ScanKey) DatumGetPointer(skey->sk_argument);
 	int32		cmpresult = 0;
@@ -2708,7 +3041,11 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 
 		if (isNull)
 		{
-			if (subkey->sk_flags & SK_BT_NULLS_FIRST)
+			if (forcenonrequired)
+			{
+				/* treating scan key as non-required */
+			}
+			else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -2762,8 +3099,12 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			 */
 			Assert(subkey != (ScanKey) DatumGetPointer(skey->sk_argument));
 			subkey--;
-			if ((subkey->sk_flags & SK_BT_REQFWD) &&
-				ScanDirectionIsForward(dir))
+			if (forcenonrequired)
+			{
+				/* treating scan key as non-required */
+			}
+			else if ((subkey->sk_flags & SK_BT_REQFWD) &&
+					 ScanDirectionIsForward(dir))
 				*continuescan = false;
 			else if ((subkey->sk_flags & SK_BT_REQBKWD) &&
 					 ScanDirectionIsBackward(dir))
@@ -2815,7 +3156,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			break;
 	}
 
-	if (!result)
+	if (!result && !forcenonrequired)
 	{
 		/*
 		 * Tuple fails this qual.  If it's a required qual for the current
@@ -2859,6 +3200,8 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	Assert(!pstate->forcenonrequired);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
-- 
2.47.2

v29-0005-DEBUG-Add-skip-scan-disable-GUCs.patchapplication/octet-stream; name=v29-0005-DEBUG-Add-skip-scan-disable-GUCs.patchDownload
From c6b5ed68050f133d091d52db6f91e29c475acf5a Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 18 Jan 2025 10:54:44 -0500
Subject: [PATCH v29 5/5] DEBUG: Add skip scan disable GUCs.

---
 src/include/access/nbtree.h                   |  5 +++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 37 +++++++++++++++++++
 src/backend/access/nbtree/nbtutils.c          |  3 ++
 src/backend/utils/misc/guc_tables.c           | 34 +++++++++++++++++
 4 files changed, 79 insertions(+)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index df653d8b2..095676b21 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1184,6 +1184,11 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+extern PGDLLIMPORT bool skipscan_iprefix_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 999bb4578..9563ed948 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -21,6 +21,33 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
+/*
+ * skipscan_iprefix_enabled can be used to disable optimizations used when the
+ * maintenance overhead of skip arrays stops paying for itself
+ */
+bool		skipscan_iprefix_enabled = true;
+
 typedef struct BTScanKeyPreproc
 {
 	ScanKey		inkey;
@@ -1644,6 +1671,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
 			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
 
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].sksup = NULL;
+
 			/*
 			 * We'll need a 3-way ORDER proc.  Set that up now.
 			 */
@@ -2148,6 +2179,12 @@ _bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
 		if (attno_has_rowcompare)
 			break;
 
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
 		/*
 		 * Now consider next attno_inkey (or keep going if this is an
 		 * additional scan key against the same attribute)
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 5d75a7f29..a0a7948b2 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -2472,6 +2472,9 @@ _bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate)
 				arrayidx = 0,
 				firstchangingattnum;
 
+	if (!skipscan_iprefix_enabled)
+		return;
+
 	Assert(so->skipScan && pstate->minoff < pstate->maxoff);
 
 	/* minoff is an offset to the lowest non-pivot tuple on the page */
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 60a40ed44..5387ffa7e 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1786,6 +1787,28 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
+	/* XXX Remove before commit */
+	{
+		{"skipscan_iprefix_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_iprefix_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3697,6 +3720,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
-- 
2.47.2

In reply to: Peter Geoghegan (#80)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Mar 19, 2025 at 5:08 PM Peter Geoghegan <pg@bowt.ie> wrote:

The tricky logic added by 0003-* is my single biggest outstanding
concern about the patch series. Particularly its potential to confuse
the existing invariants for required arrays. And particularly in light
of the changes that I made to 0003-* in the past few days.

A big part of the concern here is with the existing pstate.prechecked
optimization (the one added to Postgres 17 by Alexander Korotkov's
commit e0b1ee17). It now seems quite redundant -- the new
_bt_skip_ikeyprefix mechanism added by my 0003-* patch does the same
thing, but does it better (especially since I taught
_bt_skip_ikeyprefix to deal with simple inequalities in v29). I now
think that it makes most sense to totally replace pstate.prechecked
with _bt_skip_ikeyprefix -- we should use _bt_skip_ikeyprefix during
every scan (not just during skip scans, not just during scans with
SAOP array keys), and be done with it.

To recap, right now the so->scanBehind flag serves two roles:

1. It lets us know that it is unsafe to apply the pstate.prechecked
optimization when _bt_readpage gets to the next page.

2. It lets us know that a recheck of the finaltup tuple is required on
the next page (right now that is limited to forwards scans that
encounter a truncated high key, but I intend to expand it in the
0001-* patch to other cases where we want to move onto the next page
without being 100% sure that it's the right thing to do).

Role #1 is related to the fact that pstate.prechecked works in a way
that doesn't really know anything about SAOP array keys. My Postgres
17 commit 5bf748b8 added role #1, to work around the problem of the
precheck being confused in cases where the scan's array keys are
advanced before we reach the final tuple on the page (the precheck
tuple). That way, scans with array keys were still able to safely use
the pstate.prechecked optimization, at least in simpler cases.

The new, similar _bt_skip_ikeyprefix mechanism has no need to check
scanBehind like this. That's because it is directly aware of both SAOP
arrays and skip arrays (it doesn't actually examine the sk_argument
from a scan key associated with an array, it just looks at the array
itself). That's a big advantage -- especially in light of the
scheduling improvements from 0001-*, which will make many more
_bt_readpage calls see the so->scanBehind flag as set (making us not
use pstate.prechecked). The new scheduling stuff from 0001-* affects
scans with skip arrays and SAOP arrays in just the same way, and yet
right now there's an unhelpful disconnect between what each case will
do once it reaches the next page. This is probably the single biggest
point that makes pstate.prechecked seem like it clashes with
_bt_skip_ikeyprefix: it undermines the principle that array type (SAOP
array vs skip array) should not matter anywhere outside of the lowest
level nbtutils.c code, which is a principle that now seems important.

Another key advantage of the _bt_skip_ikeyprefix mechanism is that it
can plausibly work in many cases that pstate.prechecked currently
can't handle at all. The design of pstate.prechecked makes it an "all
or nothing" thing -- it either works for every scan key required in
the scan direction, or it works for none at all. Not so for
_bt_skip_ikeyprefix; it can set pstate.ikey to skip a prefix of = scan
keys (a prefix of keys known to satisfy all tuples on pstate.page),
without the prefix necessarily including every = scan key. That
flexibility could matter a lot.

Furthermore, it can do this (set pstate.ikey to a value greater than
0) *without* needing to set pstate.forcenonrequired to make it safe
(pstate.forcenonrequired must be set during scans with array keys to
set pstate.ikey to a non-zero value, but otherwise doesn't need to be
set). In other words, it's possible for most scans to use
_bt_skip_ikeyprefix without having to commit themselves to scan all of
the tuples on the page (unlike pstate.prechecked).

I'm pretty sure that this is the right direction already, for the
reasons given, but also because I already have a draft version that
passes all tests. It significantly improves performance in many of my
most important microbenchmarks: unsympathetic cases that previously
had 5%-10% regression in query execution time end up with only 2% - 5%
regressions. I think that that's because the draft patch completely
removes the "prechecked" code path from _bt_check_compare, which is a
very hot function -- especially during these adversarial
microbenchmarks.

Under this new scheme, so->scanBehind is strictly a flag that
indicates that a recheck is scheduled, to be performed once the scan
calls _bt_readpage for the next page. It no longer serves role #1,
only role #2. That seems significantly simpler.

Note that I'm *not* proposing to remove/replace the similar
pstate.firstmatch optimization (also added by Alexander Korotkov's
commit e0b1ee17). That's still independently useful (it'll complement
_bt_skip_ikeyprefix in just the same way as it complemented
pstate.prechecked).

--
Peter Geoghegan

In reply to: Peter Geoghegan (#81)
4 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Mar 21, 2025 at 11:36 AM Peter Geoghegan <pg@bowt.ie> wrote:

A big part of the concern here is with the existing pstate.prechecked
optimization (the one added to Postgres 17 by Alexander Korotkov's
commit e0b1ee17). It now seems quite redundant -- the new
_bt_skip_ikeyprefix mechanism added by my 0003-* patch does the same
thing, but does it better (especially since I taught
_bt_skip_ikeyprefix to deal with simple inequalities in v29). I now
think that it makes most sense to totally replace pstate.prechecked
with _bt_skip_ikeyprefix -- we should use _bt_skip_ikeyprefix during
every scan (not just during skip scans, not just during scans with
SAOP array keys), and be done with it.

I just committed "Improve nbtree array primitive scan scheduling".

Attached is v30, which fully replaces the pstate.prechecked
optimization with the new _bt_skip_ikeyprefix optimization (which now
appears in v30-0002-Lower-nbtree-skip-array-maintenance-overhead.patch,
and not in 0003-*, due to my committing the primscan scheduling patch
just now).

I'm now absolutely convinced that fully generalizing
_bt_skip_ikeyprefix (as described in yesterday's email) is the right
direction to take things in. It seems to have no possible downside.

Under this new scheme, so->scanBehind is strictly a flag that
indicates that a recheck is scheduled, to be performed once the scan
calls _bt_readpage for the next page. It no longer serves role #1,
only role #2. That seems significantly simpler.

I especially like this about the new _bt_skip_ikeyprefix scheme.
Having so->scanBehind strictly be a flag (that tracks if we need a
recheck at the start of reading the next page) substantially lowers
the cognitive burden for somebody trying to understand how the
primitive scan scheduling stuff works.

The newly expanded _bt_skip_ikeyprefix needs quite a bit more testing
and polishing to be committable. I didn't even update the relevant
commit message for v30. Plus I'm not completely sure what to do about
RowCompare keys just yet, which have some funny rules when dealing
with NULLs.

--
Peter Geoghegan

Attachments:

v30-0004-DEBUG-Add-skip-scan-disable-GUCs.patchapplication/octet-stream; name=v30-0004-DEBUG-Add-skip-scan-disable-GUCs.patchDownload
From 8e2e132165d221693d13013ec286550304312326 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 18 Jan 2025 10:54:44 -0500
Subject: [PATCH v30 4/4] DEBUG: Add skip scan disable GUCs.

---
 src/include/access/nbtree.h                   |  5 +++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 37 +++++++++++++++++++
 src/backend/access/nbtree/nbtutils.c          |  3 ++
 src/backend/utils/misc/guc_tables.c           | 34 +++++++++++++++++
 4 files changed, 79 insertions(+)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index ec0f095ba..0851532e4 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1183,6 +1183,11 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+extern PGDLLIMPORT bool skipscan_iprefix_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 12be4b529..91f0f95dd 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -21,6 +21,33 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
+/*
+ * skipscan_iprefix_enabled can be used to disable optimizations used when the
+ * maintenance overhead of skip arrays stops paying for itself
+ */
+bool		skipscan_iprefix_enabled = true;
+
 typedef struct BTScanKeyPreproc
 {
 	ScanKey		inkey;
@@ -1643,6 +1670,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
 			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
 
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].sksup = NULL;
+
 			/*
 			 * We'll need a 3-way ORDER proc.  Set that up now.
 			 */
@@ -2147,6 +2178,12 @@ _bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
 		if (attno_has_rowcompare)
 			break;
 
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
 		/*
 		 * Now consider next attno_inkey (or keep going if this is an
 		 * additional scan key against the same attribute)
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index b46bfe492..9af425ec7 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -2455,6 +2455,9 @@ _bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate)
 	if (so->numberOfKeys == 0)
 		return;
 
+	if (!skipscan_iprefix_enabled)
+		return;
+
 	/* minoff is an offset to the lowest non-pivot tuple on the page */
 	iid = PageGetItemId(pstate->page, pstate->minoff);
 	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 17d28f458..a30560c7f 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1788,6 +1789,28 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
+	/* XXX Remove before commit */
+	{
+		{"skipscan_iprefix_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_iprefix_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3734,6 +3757,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
-- 
2.49.0

v30-0003-Apply-low-order-skip-key-in-_bt_first-more-often.patchapplication/octet-stream; name=v30-0003-Apply-low-order-skip-key-in-_bt_first-more-often.patchDownload
From 4052860dde1a7d841667bb7995cc85942150d24f Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 13 Jan 2025 16:08:32 -0500
Subject: [PATCH v30 3/4] Apply low-order skip key in _bt_first more often.

Convert low_compare and high_compare nbtree skip array inequalities
(with opclasses that offer skip support) such that _bt_first is
consistently able to use later keys when descending the tree within
_bt_first.

For example, an index qual "WHERE a > 5 AND b = 2" is now converted to
"WHERE a >= 6 AND b = 2" by a new preprocessing step that takes place
after a final low_compare and/or high_compare are chosen by all earlier
preprocessing steps.  That way the scan's initial call to _bt_first will
use "WHERE a >= 6 AND b = 2" to find the initial leaf level position,
rather than merely using "WHERE a > 5" -- "b = 2" can always be applied.
This has a decent chance of making the scan avoid an extra _bt_first
call that would otherwise be needed just to determine the lowest-sorting
"a" value in the index (the lowest that still satisfies "WHERE a > 5").

The transformation process can only lower the total number of index
pages read when the use of a more restrictive set of initial positioning
keys in _bt_first actually allows the scan to land on some later leaf
page directly, relative to the unoptimized case (or on an earlier leaf
page directly, when scanning backwards).  The savings can be far greater
when affected skip arrays come after some higher-order array.  For
example, a qual "WHERE x IN (1, 2, 3) AND y > 5 AND z = 2" can now save
as many as 3 _bt_first calls as a result of these transformations (there
can be as many as 1 _bt_first call saved per "x" array element).

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=FJ78K3WsF3iWNxWnUCY9f=Jdg3QPxaXE=uYUbmuRz5Q@mail.gmail.com
---
 src/backend/access/nbtree/nbtpreprocesskeys.c | 180 ++++++++++++++++++
 src/test/regress/expected/create_index.out    |  21 ++
 src/test/regress/sql/create_index.sql         |  10 +
 3 files changed, 211 insertions(+)

diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 963fce8a9..12be4b529 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -50,6 +50,12 @@ static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
 								 BTArrayKeyInfo *array, bool *qual_ok);
 static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
 								 BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+									   BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
@@ -1291,6 +1297,171 @@ _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
 	return true;
 }
 
+/*
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts their
+ * operator strategies, while changing the operator as appropriate).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value MINVAL, using a transformed >= key
+ * instead of using the original > key makes it safe to include lower-order
+ * scan keys in the insertion scan key (there must be lower-order scan keys
+ * after the skip array).  We will avoid an extra _bt_first to find the first
+ * value in the index > sk_argument -- at least when the first real matching
+ * value in the index happens to be an exact match for the sk_argument value
+ * that we produced here by incrementing the original input key's sk_argument.
+ * (Backwards scans derive the same benefit when they encounter the sentinel
+ * value MAXVAL, by converting the high_compare key from < to <=.)
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01'.
+ *
+ * Note: We can safely modify array->low_compare/array->high_compare in place
+ * because they just point to copies of our scan->keyData[] input scan keys
+ * (namely the copies returned by _bt_preprocess_array_keys to be used as
+ * input into the standard preprocessing steps in _bt_preprocess_keys).
+ * Everything will be reset if there's a rescan.
+ */
+static void
+_bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+						   BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	/*
+	 * Called last among all preprocessing steps, when the skip array's final
+	 * low_compare and high_compare have both been chosen
+	 */
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1 && !array->null_elem && array->sksup);
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skiparray_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skiparray_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1825,6 +1996,15 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && array->sksup &&
+						!array->null_elem)
+						_bt_skiparray_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 15dde752f..fa4517e2d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2589,6 +2589,27 @@ ORDER BY thousand;
         1 |     1001
 (1 row)
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+                                            QUERY PLAN                                            
+--------------------------------------------------------------------------------------------------
+ Limit
+   ->  Index Only Scan using tenk1_thous_tenthous on tenk1
+         Index Cond: ((thousand > '-1'::integer) AND (tenthous = ANY ('{1001,3000}'::integer[])))
+(3 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+        1 |     1001
+(2 rows)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index 40ba3d65b..e8c16959a 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -993,6 +993,16 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
 ORDER BY thousand;
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
-- 
2.49.0

v30-0002-Lower-nbtree-skip-array-maintenance-overhead.patchapplication/octet-stream; name=v30-0002-Lower-nbtree-skip-array-maintenance-overhead.patchDownload
From 51baee1800b14313b9d03daed238d81ac48d5c8e Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v30 2/4] Lower nbtree skip array maintenance overhead.

Add an optimization that fixes regressions in index scans that are
nominally eligible to use skip scan, but can never actually benefit from
skipping.  These are cases where a leading prefix column contains many
distinct values -- especially when the number of values approaches the
total number of index tuples, where skipping can never be profitable.

The optimization is activated dynamically, as a fallback strategy.  It
works by determining a prefix of leading index columns whose scan keys
(often skip array scan keys) are guaranteed to be satisfied by every
possible index tuple on a given page.  _bt_readpage is then able to
start comparisons at the first scan key that might not be satisfied.
This necessitates making _bt_readpage temporarily cease maintaining the
scan's arrays.  _bt_checkkeys will treat the scan's keys as if they were
not marked as required during preprocessing.  This process relies on the
non-required SAOP array logic in _bt_advance_array_keys that was added
to Postgres 17 by commit 5bf748b8.

The new optimization does not affect array primitive scan scheduling.
It is similar to the precheck optimization added by Postgres 17 commit
e0b1ee17dc, though it is only used during nbtree scans with skip arrays.
It can be applied during scans that were never eligible for the precheck
optimization.  As a result, many scans that cannot benefit from skipping
will still benefit from using skip arrays (skip arrays indirectly enable
the use of the optimization introduced by this commit).

Skip scan's approach of adding skip arrays during preprocessing and then
fixing (or significantly ameliorating) the resulting regressions seen in
unsympathetic cases is enabled by the optimization added by this commit
(and by the "look ahead" optimization introduced by commit 5bf748b8).
This allows the planner to avoid generating distinct, competing index
paths (one path for skip scan, another for an equivalent traditional
full index scan).  The overall effect is to make scan runtime close to
optimal, even when the planner works off an incorrect cardinality
estimate.  Scans will also perform well given a skipped column with data
skew: individual groups of pages with many distinct values in respect of
a skipped column can be read about as efficiently as before, without
having to give up on skipping over other provably-irrelevant leaf pages.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
---
 src/include/access/nbtree.h                   |   9 +-
 src/backend/access/nbtree/nbtpreprocesskeys.c |   2 +
 src/backend/access/nbtree/nbtree.c            |   1 +
 src/backend/access/nbtree/nbtsearch.c         |  65 ++-
 src/backend/access/nbtree/nbtutils.c          | 483 +++++++++++++++---
 5 files changed, 444 insertions(+), 116 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index b86bf7bf3..ec0f095ba 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1059,6 +1059,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		rowCompare;		/* At least one RowCompare key in keyData[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Check scan not still behind on next page? */
 	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
@@ -1105,6 +1106,8 @@ typedef struct BTReadPageState
 	IndexTuple	finaltup;		/* Needed by scans with array keys */
 	Page		page;			/* Page being read */
 	bool		firstpage;		/* page is first for primitive scan? */
+	bool		forcenonrequired;	/* treat all scan keys as nonrequired? */
+	int			ikey;			/* start comparisons from ikey'th scan key */
 
 	/* Per-tuple input parameters, set by _bt_readpage for _bt_checkkeys */
 	OffsetNumber offnum;		/* current tuple's page offset number */
@@ -1114,10 +1117,9 @@ typedef struct BTReadPageState
 	bool		continuescan;	/* Terminate ongoing (primitive) index scan? */
 
 	/*
-	 * Input and output parameters, set and unset by both _bt_readpage and
-	 * _bt_checkkeys to manage precheck optimizations
+	 * Input and output parameter, set and unset by both _bt_readpage and
+	 * _bt_checkkeys for "key required in opposite direction" optimization
 	 */
-	bool		prechecked;		/* precheck set continuescan to 'true'? */
 	bool		firstmatch;		/* at least one match so far?  */
 
 	/*
@@ -1327,6 +1329,7 @@ extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arra
 						  IndexTuple tuple, int tupnatts);
 extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
 									 IndexTuple finaltup);
+extern void _bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 7bd121f83..963fce8a9 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -466,6 +466,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			if (numberOfEqualCols == attno - 1)
 				_bt_mark_scankey_required(outkey);
 
+			so->rowCompare = true;
+
 			/*
 			 * We don't support RowCompare using equality; such a qual would
 			 * mess up the numberOfEqualCols tracking.
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 815cbcfb7..d714fc4ad 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -349,6 +349,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	else
 		so->keyData = NULL;
 
+	so->rowCompare = false;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->oppositeDirCheck = false;
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 2524cb23a..3491dac78 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1643,47 +1643,15 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.finaltup = NULL;
 	pstate.page = page;
 	pstate.firstpage = firstpage;
+	pstate.forcenonrequired = false;
+	pstate.ikey = 0;
 	pstate.offnum = InvalidOffsetNumber;
 	pstate.skip = InvalidOffsetNumber;
 	pstate.continuescan = true; /* default assumption */
-	pstate.prechecked = false;
 	pstate.firstmatch = false;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
-	/*
-	 * Prechecking the value of the continuescan flag for the last item on the
-	 * page (for backwards scan it will be the first item on a page).  If we
-	 * observe it to be true, then it should be true for all other items. This
-	 * allows us to do significant optimizations in the _bt_checkkeys()
-	 * function for all the items on the page.
-	 *
-	 * With the forward scan, we do this check for the last item on the page
-	 * instead of the high key.  It's relatively likely that the most
-	 * significant column in the high key will be different from the
-	 * corresponding value from the last item on the page.  So checking with
-	 * the last item on the page would give a more precise answer.
-	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.  Also avoid it during scans with array keys,
-	 * which might be using skip scan (XXX fixed in next commit).
-	 */
-	if (!pstate.firstpage && !arrayKeys && minoff < maxoff)
-	{
-		ItemId		iid;
-		IndexTuple	itup;
-
-		iid = PageGetItemId(page, ScanDirectionIsForward(dir) ? maxoff : minoff);
-		itup = (IndexTuple) PageGetItem(page, iid);
-
-		/* Call with arrayKeys=false to avoid undesirable side-effects */
-		_bt_checkkeys(scan, &pstate, false, itup, indnatts);
-		pstate.prechecked = pstate.continuescan;
-		pstate.continuescan = true; /* reset */
-	}
-
 	if (ScanDirectionIsForward(dir))
 	{
 		/* SK_SEARCHARRAY forward scans must provide high key up front */
@@ -1711,6 +1679,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
+		/*
+		 * Consider pstate.ikey optimization once the ongoing primitive index
+		 * scan has already read at least one page
+		 */
+		if (!pstate.firstpage && minoff < maxoff)
+			_bt_skip_ikeyprefix(scan, &pstate);
+
 		/* load items[] in ascending order */
 		itemIndex = 0;
 
@@ -1747,6 +1722,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum < pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1810,8 +1786,12 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			IndexTuple	itup = (IndexTuple) PageGetItem(page, iid);
 			int			truncatt;
 
+			/* stop treating required keys as nonrequired */
+			pstate.forcenonrequired = false;
+			pstate.firstmatch = false;
+			pstate.ikey = 0;
+
 			truncatt = BTreeTupleGetNAtts(itup, rel);
-			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -1850,6 +1830,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
+		/*
+		 * Consider pstate.ikey optimization once the ongoing primitive index
+		 * scan has already read at least one page
+		 */
+		if (!pstate.firstpage && minoff < maxoff)
+			_bt_skip_ikeyprefix(scan, &pstate);
+
 		/* load items[] in descending order */
 		itemIndex = MaxTIDsPerBTreePage;
 
@@ -1889,6 +1876,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff)
+			{
+				/* stop treating required keys as nonrequired */
+				pstate.forcenonrequired = false;
+				pstate.firstmatch = false;
+				pstate.ikey = 0;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
@@ -1900,6 +1894,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum > pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 62530702f..b46bfe492 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -57,11 +57,11 @@ static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
-							  bool *continuescan, int *ikey);
+							  bool advancenonrequired, bool forcenonrequired,
+							  bool firstmatch, bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-								 ScanDirection dir, bool *continuescan);
+								 ScanDirection dir, bool forcenonrequired, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -1421,9 +1421,10 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
  * postcondition's <= operator with a >=.  In other words, just swap the
  * precondition with the postcondition.)
  *
- * We also deal with "advancing" non-required arrays here.  Callers whose
- * sktrig scan key is non-required specify sktrig_required=false.  These calls
- * are the only exception to the general rule about always advancing the
+ * We also deal with "advancing" non-required arrays here (or arrays that are
+ * treated as non-required for the duration of a _bt_readpage call).  Callers
+ * whose sktrig scan key is non-required specify sktrig_required=false.  These
+ * calls are the only exception to the general rule about always advancing the
  * required array keys (the scan may not even have a required array).  These
  * callers should just pass a NULL pstate (since there is never any question
  * of stopping the scan).  No call to _bt_tuple_before_array_skeys is required
@@ -1480,8 +1481,8 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		pstate->firstmatch = false;
 
-		/* Shouldn't have to invalidate 'prechecked', though */
-		Assert(!pstate->prechecked);
+		/* Shouldn't have to invalidate forcenonrequired state */
+		Assert(!pstate->forcenonrequired && pstate->ikey == 0);
 
 		/*
 		 * Once we return we'll have a new set of required array keys, so
@@ -1490,6 +1491,26 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		pstate->rechecks = 0;
 		pstate->targetdistance = 0;
 	}
+	else if (sktrig < so->numberOfKeys - 1 &&
+			 !(so->keyData[so->numberOfKeys - 1].sk_flags & SK_SEARCHARRAY))
+	{
+		int			least_sign_ikey = so->numberOfKeys - 1;
+		bool		continuescan;
+
+		/*
+		 * Optimization: perform a precheck of the least significant key
+		 * during !sktrig_required calls when it isn't already our sktrig
+		 * (provided the precheck key is not itself an array).
+		 *
+		 * When the precheck works out we'll avoid an expensive binary search
+		 * of sktrig's array (plus any other arrays before least_sign_ikey).
+		 */
+		Assert(so->keyData[sktrig].sk_flags & SK_SEARCHARRAY);
+		if (!_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
+							   false, false, false,
+							   &continuescan, &least_sign_ikey))
+			return false;
+	}
 
 	Assert(_bt_verify_keys_with_arraykeys(scan));
 
@@ -1533,8 +1554,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -1668,7 +1687,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -1710,7 +1729,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -1725,7 +1744,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -1785,6 +1804,12 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * of any required scan key).  All that matters is whether caller's tuple
 	 * satisfies the new qual, so it's safe to just skip the _bt_check_compare
 	 * recheck when we've already determined that it can only return 'false'.
+	 *
+	 * Note: In practice most scan keys are marked required by preprocessing,
+	 * if necessary by generating a preceding skip array.  We nevertheless
+	 * often handle array keys marked required as if they were nonrequired.
+	 * This behavior is requested by our _bt_check_compare caller, though only
+	 * when it is passed "forcenonrequired=true" by _bt_checkkeys.
 	 */
 	if ((sktrig_required && all_required_satisfied) ||
 		(!sktrig_required && all_satisfied))
@@ -1796,7 +1821,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
+							  false, !sktrig_required, false,
 							  &continuescan, &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -2041,8 +2066,9 @@ new_prim_scan:
 	 * read at least one leaf page before the one we're reading now.  This
 	 * makes primscan scheduling more efficient when scanning subsets of an
 	 * index with many distinct attribute values matching many array elements.
-	 * It encourages fewer, larger primitive scans where that makes sense
-	 * (where index descent costs need to be kept under control).
+	 * It encourages fewer, larger primitive scans where that makes sense.
+	 * This will in turn encourage _bt_readpage to apply the pstate.ikey
+	 * optimization more often.
 	 *
 	 * Note: This heuristic isn't as aggressive as you might think.  We're
 	 * conservative about allowing a primitive scan to step from the first
@@ -2199,17 +2225,14 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
  * the page to the right.
  *
  * Advances the scan's array keys when necessary for arrayKeys=true callers.
- * Caller can avoid all array related side-effects when calling just to do a
- * page continuescan precheck -- pass arrayKeys=false for that.  Scans without
- * any arrays keys must always pass arrayKeys=false.
+ * Scans without any array keys must always pass arrayKeys=false.
  *
  * Also stops and starts primitive index scans for arrayKeys=true callers.
  * Scans with array keys are required to set up page state that helps us with
  * this.  The page's finaltup tuple (the page high key for a forward scan, or
  * the page's first non-pivot tuple for a backward scan) must be set in
- * pstate.finaltup ahead of the first call here for the page (or possibly the
- * first call after an initial continuescan-setting page precheck call).  Set
- * this to NULL for rightmost page (or the leftmost page for backwards scans).
+ * pstate.finaltup ahead of the first call here for the page.  Set this to
+ * NULL for rightmost page (or the leftmost page for backwards scans).
  *
  * scan: index scan descriptor (containing a search-type scankey)
  * pstate: page level input and output parameters
@@ -2224,42 +2247,34 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	TupleDesc	tupdesc = RelationGetDescr(scan->indexRelation);
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ScanDirection dir = so->currPos.dir;
-	int			ikey = 0;
+	int			ikey = pstate->ikey;
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
+	Assert(arrayKeys || so->numArrayKeys == 0);
 
-	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
+	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, arrayKeys,
+							pstate->forcenonrequired, pstate->firstmatch,
 							&pstate->continuescan, &ikey);
 
 #ifdef USE_ASSERT_CHECKING
-	if (!arrayKeys && so->numArrayKeys)
-	{
-		/*
-		 * This is a continuescan precheck call for a scan with array keys.
-		 *
-		 * Assert that the scan isn't in danger of becoming confused.
-		 */
-		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
-		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
-											 tupnatts, false, 0, NULL));
-	}
-	if (pstate->prechecked || pstate->firstmatch)
+	if ((pstate->firstmatch || pstate->ikey > 0) && !pstate->forcenonrequired)
 	{
 		bool		dcontinuescan;
 		int			dikey = 0;
 
 		/*
-		 * Call relied on continuescan/firstmatch prechecks -- assert that we
-		 * get the same answer without those optimizations
+		 * _bt_check_compare call relied on the firstmatch and/or pstate->ikey
+		 * optimizations.  Assert that _bt_check_compare gives the same answer
+		 * when it can apply neither optimization.
 		 */
 		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
 										false, false, false,
 										&dcontinuescan, &dikey));
 		Assert(pstate->continuescan == dcontinuescan);
+		Assert(arrayKeys || dikey == ikey);
+		Assert(dikey >= ikey);	/* weaker assert is for nonrequired arrays */
 	}
 #endif
 
@@ -2280,6 +2295,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * It's also possible that the scan is still _before_ the _start_ of
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
+	Assert(!pstate->forcenonrequired);
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
@@ -2402,6 +2418,291 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	return true;
 }
 
+/*
+ * Determine a prefix of scan keys that are guaranteed to be satisfied by
+ * every possible tuple on pstate's page.
+ *
+ * Sets pstate.ikey (and pstate.forcenonrequired) on success, making later
+ * calls to _bt_checkkeys start checks of each tuple from the so->keyData[]
+ * entry at pstate.ikey (while treating keys >= pstate.ikey as nonrequired).
+ *
+ * Caller must reset pstate.ikey and pstate.forcenonrequired just ahead of the
+ * _bt_checkkeys call for the page's final tuple (the pstate.finaltup tuple).
+ * That will give _bt_checkkeys the opportunity to call _bt_advance_array_keys
+ * properly (with sktrig_required=true) should the array keys need to advance
+ * on this page.  Caller doesn't need to do this on the rightmost/leftmost
+ * page in the index (where pstate.finaltup won't ever be set).
+ */
+void
+_bt_skip_ikeyprefix(IndexScanDesc scan, BTReadPageState *pstate)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	ScanDirection dir = so->currPos.dir;
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	ItemId		iid;
+	IndexTuple	firsttup,
+				lasttup;
+	int			ikey = 0,
+				arrayidx = 0,
+				firstchangingattnum;
+
+	Assert(pstate->minoff < pstate->maxoff);
+
+	if (so->rowCompare && so->numArrayKeys)
+		return;
+
+	if (so->numberOfKeys == 0)
+		return;
+
+	/* minoff is an offset to the lowest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->minoff);
+	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* maxoff is an offset to the highest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->maxoff);
+	lasttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* Determine the first attribute whose values change on caller's page */
+	firstchangingattnum = _bt_keep_natts_fast(rel, firsttup, lasttup);
+
+	for (; ikey < so->numberOfKeys; ikey++)
+	{
+		ScanKey		key = so->keyData + ikey;
+		BTArrayKeyInfo *array;
+		Datum		tupdatum;
+		bool		tupnull;
+		int32		result;
+
+		if (key->sk_flags & SK_ROW_HEADER)
+		{
+			break;				/* pstate.ikey to be set to RowCompare ikey */
+		}
+
+		/*
+		 * Determine if it's safe to set pstate.ikey to an offset to a key
+		 * that comes after this key, by examining this key
+		 */
+		if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) == 0)
+		{
+			/*
+			 * This is the first key that is not marked required (only happens
+			 * when _bt_preprocess_keys couldn't mark all keys required due to
+			 * implementation restrictions affecting skip array generation)
+			 */
+			Assert(!(key->sk_flags & SK_BT_SKIP));
+			break;				/* pstate.ikey to be set to nonrequired ikey */
+		}
+		if (key->sk_strategy != BTEqualStrategyNumber)
+		{
+			/*
+			 * This is the scan's first inequality key (barring inequalities
+			 * that are used to describe the range of a = skip array key).
+			 *
+			 * It's definitely safe for _bt_checkkeys to avoid assessing this
+			 * inequality when the page's first and last non-pivot tuples both
+			 * satisfy the inequality (since the same must also be true of all
+			 * the tuples in between these two).
+			 *
+			 * Unlike the "=" case, it doesn't matter if this attribute has
+			 * more than one distinct value (though it is necessary for any
+			 * and all _prior_ attributes to contain no more than one distinct
+			 * value amongst all of the tuples from pstate.page).
+			 */
+			if (key->sk_attno > firstchangingattnum)
+				break;			/* pstate.ikey to be set to inequality's ikey */
+
+			if (key->sk_flags & SK_ISNULL)
+			{
+				Assert(key->sk_flags & SK_SEARCHNOTNULL);
+				break;
+			}
+
+			tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &tupnull);
+			if (tupnull)
+				break;			/* pstate.ikey to be set to inequality's ikey */
+			if (!DatumGetBool(FunctionCall2Coll(&key->sk_func,
+												key->sk_collation, tupdatum,
+												key->sk_argument)))
+				break;
+
+			tupdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &tupnull);
+			if (tupnull)
+				break;			/* pstate.ikey to be set to inequality's ikey */
+
+			if (!DatumGetBool(FunctionCall2Coll(&key->sk_func,
+												key->sk_collation, tupdatum,
+												key->sk_argument)))
+				break;			/* pstate.ikey to be set to inequality's ikey */
+
+			continue;			/* safe, key satisfied by every tuple */
+		}
+		if (!(key->sk_flags & SK_SEARCHARRAY))
+		{
+			/*
+			 * Found a scalar (non-array) = key.
+			 *
+			 * It is unsafe to set pstate.ikey to an ikey beyond this key,
+			 * unless the = key is satisfied by every possible tuple on the
+			 * page (possible only when attribute has just one distinct value
+			 * among all tuples on the page).
+			 */
+			if (key->sk_attno < firstchangingattnum)
+			{
+				/*
+				 * Only one distinct value on the page for this key's column.
+				 * Must still make sure that = key is actually satisfied by
+				 * the value that is stored within every tuple on the page.
+				 */
+				tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+										 &tupnull);
+				if (key->sk_flags & SK_ISNULL)
+				{
+					Assert(key->sk_flags & SK_SEARCHNULL);
+					if (tupnull)
+						continue;
+					break;
+				}
+				if (!DatumGetBool(FunctionCall2Coll(&key->sk_func,
+													key->sk_collation, tupdatum,
+													key->sk_argument)))
+					break;		/* pstate.ikey to be set to equality's ikey */
+
+				continue;		/* safe, key satisfied by every tuple */
+			}
+			break;				/* pstate.ikey to be set to scalar key's ikey */
+		}
+
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == ikey);
+		if (array->num_elems != -1)
+		{
+			/*
+			 * Found a SAOP array = key.
+			 *
+			 * Handle this just like we handle scalar = keys.
+			 */
+			if (key->sk_attno < firstchangingattnum)
+			{
+				/*
+				 * Only one distinct value on the page for this key's column.
+				 * Must still make sure that SAOP array is actually satisfied
+				 * by the value that is stored within every tuple on the page.
+				 */
+				tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+										 &tupnull);
+				_bt_binsrch_array_skey(&so->orderProcs[ikey], false,
+									   NoMovementScanDirection,
+									   tupdatum, tupnull, array, key, &result);
+				if (result == 0)
+					continue;	/* safe, SAOP = key satisfied by every tuple */
+			}
+			break;				/* pstate.ikey to be set to SAOP array's ikey */
+		}
+
+		/*
+		 * Found a skip array = key.
+		 *
+		 * As with other = keys, moving past this skip array key is safe when
+		 * every tuple on the page is guaranteed to satisfy the array's key.
+		 * But we can be more aggressive here, since skip arrays make it easy
+		 * to assess whether all the values on the page fall within the skip
+		 * array's entire range.
+		 */
+		if (array->null_elem)
+		{
+			/* Safe, non-range skip array "satisfied" by every tuple on page */
+			continue;
+		}
+		else if (key->sk_attno > firstchangingattnum)
+		{
+			/*
+			 * We cannot assess whether this range skip array will definitely
+			 * be satisfied by every tuple on the page, since its attribute is
+			 * preceded by another attribute that is not certain to contain
+			 * the same prefix of value(s) within every tuple from pstate.page
+			 */
+			break;				/* pstate.ikey to be set to range array's ikey */
+		}
+
+		/*
+		 * Found a range skip array = key.
+		 *
+		 * This is just like the inequality case (though the inequality keys
+		 * tested here are in the array itself, not in so->keyData[]).
+		 */
+		tupdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   tupdatum, tupnull, array, key, &result);
+		if (result != 0)
+			break;				/* pstate.ikey to be set to range array's ikey */
+
+		tupdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &tupnull);
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   tupdatum, tupnull, array, key, &result);
+		if (result != 0)
+			break;				/* pstate.ikey to be set to range array's ikey */
+
+		/* Safe, range skip array satisfied by every tuple on page */
+	}
+
+	/*
+	 * If pstate.ikey remains 0, _bt_advance_array_keys will still be able to
+	 * apply its precheck optimization when dealing with "nonrequired" array
+	 * keys.  That's reason enough on its own to set forcenonrequired=true.
+	 */
+	pstate->forcenonrequired = (so->numArrayKeys > 0);
+	pstate->ikey = ikey;
+
+	if (!pstate->forcenonrequired)
+		return;
+
+	/*
+	 * Set the element for range skip arrays whose ikey is >= pstate.ikey to
+	 * whatever the first array element is in the scan's current direction.
+	 * This allows range skip arrays that will never be satisfied by any tuple
+	 * on the page to avoid extra sk_argument comparisons -- _bt_check_compare
+	 * won't use the key's sk_argument when the key is marked MINVAL/MAXVAL
+	 * (note that MINVAL/MAXVAL won't be unset until an exact match is found,
+	 * which might not happen for any tuple on the page).
+	 *
+	 * Set the element for non-range skip arrays whose ikey is >= pstate.ikey
+	 * to NULL (regardless of whether NULLs are stored first or last), too.
+	 * This allows non-range skip arrays (recognized by _bt_check_compare as
+	 * "non-required" skip arrays with ISNULL set) to avoid needlessly calling
+	 * _bt_advance_array_keys (it isn't necessary because we know that any
+	 * non-range skip array must be satisfied by every possible value).
+	 *
+	 * Note: It's safe for us to set ISNULL like this without regard for
+	 * whether it'll leave the array keys before or after the page's keyspace.
+	 * We know that once caller unsets pstate.forcenonrequired, its call to
+	 * _bt_checkkeys will still be able to advance the scan's array keys,
+	 * without hindrance from _bt_tuple_before_array_skeys.  We also know that
+	 * a sktrig_required=false call to _bt_advance_array_keys won't alter any
+	 * array unless an exact match is available to "advance" the array to.
+	 */
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		key = &so->keyData[array->scan_key];
+
+		Assert(key->sk_flags & SK_SEARCHARRAY);
+
+		if (array->num_elems != -1)
+			continue;
+		if (array->scan_key < pstate->ikey)
+			continue;
+
+		Assert(key->sk_flags & SK_BT_SKIP);
+
+		if (!array->null_elem)
+			_bt_array_set_low_or_high(rel, key, array,
+									  ScanDirectionIsForward(dir));
+		else
+			_bt_skiparray_set_isnull(rel, key, array);
+	}
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
@@ -2433,18 +2734,25 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  *
  * Though we advance non-required array keys on our own, that shouldn't have
  * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
+ * have no fixed relationship with the scan's progress.
  *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass forcenonrequired=true to instruct us to treat all keys as nonrequired.
+ * This is used to make it safe to temporarily stop properly maintaining the
+ * scan's required arrays.  Callers can determine which prefix of keys must
+ * satisfy every possible prefix of index attribute values on the page, and
+ * then pass us an initial *ikey for the first key that might be unsatisfied.
+ * We won't be maintaining any arrays before that initial *ikey, so there is
+ * no point in trying to do so for any later arrays.  (Callers that do this
+ * must be careful to reset the array keys when they finish reading the page.)
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
-				  bool *continuescan, int *ikey)
+				  bool advancenonrequired, bool forcenonrequired,
+				  bool firstmatch, bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
@@ -2460,36 +2768,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when we're forced to treat all scan keys as nonrequired)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (forcenonrequired)
+		{
+			/* treating scan's keys as non-required */
+		}
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
 			requiredOppositeDirOnly = true;
 
-		/*
-		 * If the caller told us the *continuescan flag is known to be true
-		 * for the last item on the page, then we know the keys required for
-		 * the current direction scan should be matched.  Otherwise, the
-		 * *continuescan flag would be set for the current item and
-		 * subsequently the last item on the page accordingly.
-		 *
-		 * If the key is required for the opposite direction scan, we can skip
-		 * the check if the caller tells us there was already at least one
-		 * matching item on the page. Also, we require the *continuescan flag
-		 * to be true for the last item on the page to know there are no
-		 * NULLs.
-		 *
-		 * Both cases above work except for the row keys, where NULLs could be
-		 * found in the middle of matching values.
-		 */
-		if (prechecked &&
-			(requiredSameDir || (requiredOppositeDirOnly && firstmatch)) &&
-			!(key->sk_flags & SK_ROW_HEADER))
-			continue;
-
 		if (key->sk_attno > tupnatts)
 		{
 			/*
@@ -2511,6 +2803,19 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		{
 			Assert(key->sk_flags & SK_SEARCHARRAY);
 			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir || forcenonrequired);
+
+			/*
+			 * Cannot fall back on _bt_tuple_before_array_skeys when we're
+			 * treating the scan's keys as nonrequired, though.  Just handle
+			 * this like any other non-required equality-type array key.
+			 */
+			if (forcenonrequired)
+			{
+				Assert(!(key->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+				return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
+											  tupdesc, *ikey, false);
+			}
 
 			*continuescan = false;
 			return false;
@@ -2520,7 +2825,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
-									 continuescan))
+									 forcenonrequired, continuescan))
 				continue;
 			return false;
 		}
@@ -2552,9 +2857,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			 */
 			if (requiredSameDir)
 				*continuescan = false;
+			else if (unlikely(key->sk_flags & SK_BT_SKIP))
+			{
+				/*
+				 * If we're treating scan keys as nonrequired, and encounter a
+				 * skip array scan key whose current element is NULL, then it
+				 * must be a non-range skip array.  It must be satisfied, so
+				 * there's no need to call _bt_advance_array_keys to check.
+				 */
+				Assert(forcenonrequired && *ikey > 0);
+				continue;
+			}
 
 			/*
-			 * In any case, this indextuple doesn't match the qual.
+			 * This indextuple doesn't match the qual.
 			 */
 			return false;
 		}
@@ -2575,7 +2891,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -2593,7 +2909,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -2662,7 +2978,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
  */
 static bool
 _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
-					 TupleDesc tupdesc, ScanDirection dir, bool *continuescan)
+					 TupleDesc tupdesc, ScanDirection dir,
+					 bool forcenonrequired, bool *continuescan)
 {
 	ScanKey		subkey = (ScanKey) DatumGetPointer(skey->sk_argument);
 	int32		cmpresult = 0;
@@ -2702,7 +3019,11 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 
 		if (isNull)
 		{
-			if (subkey->sk_flags & SK_BT_NULLS_FIRST)
+			if (forcenonrequired)
+			{
+				/* treating scan's keys as non-required */
+			}
+			else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -2756,8 +3077,12 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			 */
 			Assert(subkey != (ScanKey) DatumGetPointer(skey->sk_argument));
 			subkey--;
-			if ((subkey->sk_flags & SK_BT_REQFWD) &&
-				ScanDirectionIsForward(dir))
+			if (forcenonrequired)
+			{
+				/* treating scan's keys as non-required */
+			}
+			else if ((subkey->sk_flags & SK_BT_REQFWD) &&
+					 ScanDirectionIsForward(dir))
 				*continuescan = false;
 			else if ((subkey->sk_flags & SK_BT_REQBKWD) &&
 					 ScanDirectionIsBackward(dir))
@@ -2809,7 +3134,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			break;
 	}
 
-	if (!result)
+	if (!result && !forcenonrequired)
 	{
 		/*
 		 * Tuple fails this qual.  If it's a required qual for the current
@@ -2853,6 +3178,8 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	Assert(!pstate->forcenonrequired);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
-- 
2.49.0

v30-0001-Add-nbtree-skip-scan-optimizations.patchapplication/octet-stream; name=v30-0001-Add-nbtree-skip-scan-optimizations.patchDownload
From 5a1f33401435530c6af17fa3bd290495097e764a Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v30 1/4] Add nbtree skip scan optimizations.

Teach nbtree composite index scans to opportunistically skip over
irrelevant sections of composite indexes given a query with an omitted
prefix column.  When nbtree is passed input scan keys derived from a
query predicate "WHERE b = 5", new nbtree preprocessing steps now output
"WHERE a = ANY(<every possible 'a' value>) AND b = 5" scan keys.  That
is, preprocessing generates a "skip array" (along with an associated
scan key) for the omitted column "a", which makes it safe to mark the
scan key on "b" as required to continue the scan.  This is far more
efficient than a traditional full index scan whenever it allows the scan
to skip over many irrelevant leaf pages, by iteratively repositioning
itself using the keys on "a" and "b" together.

A skip array has "elements" that are generated procedurally and on
demand, but otherwise works just like a regular ScalarArrayOp array.
Preprocessing can freely add a skip array before or after any input
ScalarArrayOp arrays.  Index scans with a skip array decide when and
where to reposition the scan using the same approach as any other scan
with array keys.  This design builds on the design for array advancement
and primitive scan scheduling added to Postgres 17 by commit 5bf748b8.

The core B-Tree operator classes on most discrete types generate their
array elements with the help of their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the next
possible indexable value frequently turns out to be the next value
stored in the index.  Opclasses that lack a skip support routine fall
back on having nbtree "increment" (or "decrement") a skip array's
current element by setting the NEXT (or PRIOR) scan key flag, without
directly changing the scan key's sk_argument.  These sentinel values
behave just like any other value from an array -- though they can never
locate equal index tuples (they can only locate the next group of index
tuples containing the next set of non-sentinel values that the scan's
arrays need to advance to).

Inequality scan keys can affect how skip arrays generate their values.
Their range is constrained by the inequalities.  For example, a skip
array on "a" will only use element values 1 and 2 given a qual such as
"WHERE a BETWEEN 1 AND 2 AND b = 66".  A scan using such a skip array
has almost identical performance characteristics to one with the qual
"WHERE a = ANY('{1, 2}') AND b = 66".  The scan will be much faster when
it can be executed as two selective primitive index scans instead of a
single very large index scan that reads many irrelevant leaf pages.
However, the array transformation process won't always lead to improved
performance at runtime.  Much depends on physical index characteristics.

B-Tree preprocessing is optimistic about skipping working out: it
applies static, generic rules when determining where to generate skip
arrays, which assumes that the runtime overhead of maintaining skip
arrays will pay for itself -- or lead to only a modest performance loss.
As things stand, these assumptions are much too optimistic: skip array
maintenance will lead to unacceptable regressions with unsympathetic
queries (queries whose scan has little to no chance of avoid reading
irrelevant leaf pages).  An upcoming commit will address the problems in
this area by adding a mechanism that greatly lowers the cost of array
maintenance in these unfavorable cases.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |   3 +-
 src/include/access/nbtree.h                   |  34 +-
 src/include/catalog/pg_amproc.dat             |  22 +
 src/include/catalog/pg_proc.dat               |  27 +
 src/include/utils/skipsupport.h               |  98 +++
 src/backend/access/index/indexam.c            |   3 +-
 src/backend/access/nbtree/nbtcompare.c        | 273 +++++++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 598 ++++++++++++--
 src/backend/access/nbtree/nbtree.c            | 195 ++++-
 src/backend/access/nbtree/nbtsearch.c         | 123 ++-
 src/backend/access/nbtree/nbtutils.c          | 754 ++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |   4 +
 src/backend/commands/opclasscmds.c            |  25 +
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/date.c                  |  46 ++
 src/backend/utils/adt/meson.build             |   1 +
 src/backend/utils/adt/selfuncs.c              | 490 +++++++++---
 src/backend/utils/adt/skipsupport.c           |  61 ++
 src/backend/utils/adt/timestamp.c             |  48 ++
 src/backend/utils/adt/uuid.c                  |  70 ++
 doc/src/sgml/btree.sgml                       |  34 +-
 doc/src/sgml/indexam.sgml                     |   3 +-
 doc/src/sgml/indices.sgml                     |  49 +-
 doc/src/sgml/monitoring.sgml                  |   4 +-
 doc/src/sgml/perform.sgml                     |  31 +
 doc/src/sgml/xindex.sgml                      |  16 +-
 src/test/regress/expected/alter_generic.out   |  10 +-
 src/test/regress/expected/btree_index.out     |  41 +
 src/test/regress/expected/create_index.out    | 183 ++++-
 src/test/regress/expected/psql.out            |   3 +-
 src/test/regress/sql/alter_generic.sql        |   5 +-
 src/test/regress/sql/btree_index.sql          |  21 +
 src/test/regress/sql/create_index.sql         |  63 +-
 src/tools/pgindent/typedefs.list              |   3 +
 34 files changed, 2962 insertions(+), 380 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c4a073773..52916bab7 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -214,7 +214,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index faabcb78e..b86bf7bf3 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -707,6 +708,10 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
  *	(BTOPTIONS_PROC).  These procedures define a set of user-visible
  *	parameters that can be used to control operator class behavior.  None of
  *	the built-in B-Tree operator classes currently register an "options" proc.
+ *
+ *	To facilitate more efficient B-Tree skip scans, an operator class may
+ *	choose to offer a sixth amproc procedure (BTSKIPSUPPORT_PROC).  For full
+ *	details, see src/include/utils/skipsupport.h.
  */
 
 #define BTORDER_PROC		1
@@ -714,7 +719,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1027,10 +1033,21 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
-	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for ScalarArrayOpExpr arrays */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+	int			cur_elem;		/* index of current element in elem_values */
+
+	/* fields for skip arrays, which generate element datums procedurally */
+	int16		attlen;			/* attr's length, in bytes */
+	bool		attbyval;		/* attr's FormData_pg_attribute.attbyval */
+	bool		null_elem;		/* NULL is lowest/highest element? */
+	SkipSupport sksup;			/* skip support (NULL if opclass lacks it) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1119,6 +1136,15 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on column without input = */
+
+/* SK_BT_SKIP-only flags (set and unset by array advancement) */
+#define SK_BT_MINVAL	0x00080000	/* invalid sk_argument, use low_compare */
+#define SK_BT_MAXVAL	0x00100000	/* invalid sk_argument, use high_compare */
+#define SK_BT_NEXT		0x00200000	/* positions the scan > sk_argument */
+#define SK_BT_PRIOR		0x00400000	/* positions the scan < sk_argument */
+
+/* Remaps pg_index flag bits to uppermost SK_BT_* byte */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1165,7 +1191,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 19100482b..37000639f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 890822eaf..083da24af 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2288,6 +2303,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4478,6 +4496,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6357,6 +6378,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9405,6 +9429,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..bc51847cf
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining groups of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * It usually isn't feasible to implement skip support for an opclass whose
+ * input type is continuous.  The B-Tree code falls back on next-key sentinel
+ * values for any opclass that doesn't provide its own skip support function.
+ * This isn't really an implementation restriction; there is no benefit to
+ * providing skip support for an opclass where guessing that the next indexed
+ * value is the next possible indexable value never (or hardly ever) works out.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order)
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 * Operator class decrement/increment functions will never be called with
+	 * a NULL "existing" argument, either.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern SkipSupport PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+												 bool reverse);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 55ec4c103..219df1971 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -489,7 +489,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	if (parallel_aware &&
 		indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 291cb8fc1..4da5a3c1d 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 38a87af1c..7bd121f83 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -45,8 +45,15 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
+static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
+								 ScanKey skey, FmgrInfo *orderproc,
+								 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
+								 BTArrayKeyInfo *array, bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
+							   int *numSkipArrayKeys);
 static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
 									  Oid elemtype, StrategyNumber strat,
 									  Datum *elems, int nelems);
@@ -89,6 +96,8 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -101,10 +110,16 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never generated skip array scan keys, it would be possible for "gaps"
+ * to appear that make it unsafe to mark any subsequent input scan keys
+ * (copied from scan->keyData[]) as required to continue the scan.  Prior to
+ * Postgres 18, a qual like "WHERE y = 4" always required a full index scan.
+ * It'll now become "WHERE x = ANY('{every possible x value}') and y = 4" on
+ * output, due to the addition of a skip array on "x".  This has the potential
+ * to be much more efficient than a full index scan (though it'll behave like
+ * a full index scan when there are many distinct "x" values).
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -141,7 +156,12 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -200,6 +220,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProcs[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -288,7 +316,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -345,7 +374,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -393,6 +421,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, _bt_preprocess_array_keys has already added "=" keys
+			 * sufficient to form an unbroken series of "=" constraints on all
+			 * attrs prior to the attr from the final scan->keyData[] key.
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -481,6 +514,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -489,6 +523,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -508,8 +543,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -803,6 +836,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -812,6 +848,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -885,6 +937,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -978,24 +1031,55 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
  * Compare an array scan key to a scalar scan key, eliminating contradictory
  * array elements such that the scalar scan key becomes redundant.
  *
+ * If the opfamily is incomplete we may not be able to determine which
+ * elements are contradictory.  When we return true we'll have validly set
+ * *qual_ok, guaranteeing that at least the scalar scan key can be considered
+ * redundant.  We return false if the comparison could not be made (caller
+ * must keep both scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar scan keys.
+ */
+static bool
+_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
+							   bool *qual_ok)
+{
+	Assert(arraysk->sk_attno == skey->sk_attno);
+	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
+		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
+	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
+		   skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Just call the appropriate helper function based on whether it's a SAOP
+	 * array or a skip array.  Both helpers will set *qual_ok in passing.
+	 */
+	if (array->num_elems != -1)
+		return _bt_saoparray_shrink(scan, arraysk, skey, orderproc, array,
+									qual_ok);
+	else
+		return _bt_skiparray_shrink(scan, skey, array, qual_ok);
+}
+
+/*
+ * Preprocessing of SAOP (non-skip) array scan key, used to determine which
+ * array elements are eliminated as contradictory by a non-array scalar key.
+ * _bt_compare_array_scankey_args helper function.
+ *
  * Array elements can be eliminated as contradictory when excluded by some
  * other operator on the same attribute.  For example, with an index scan qual
  * "WHERE a IN (1, 2, 3) AND a < 2", all array elements except the value "1"
  * are eliminated, and the < scan key is eliminated as redundant.  Cases where
  * every array element is eliminated by a redundant scalar scan key have an
  * unsatisfiable qual, which we handle by setting *qual_ok=false for caller.
- *
- * If the opfamily doesn't supply a complete set of cross-type ORDER procs we
- * may not be able to determine which elements are contradictory.  If we have
- * the required ORDER proc then we return true (and validly set *qual_ok),
- * guaranteeing that at least the scalar scan key can be considered redundant.
- * We return false if the comparison could not be made (caller must keep both
- * scan keys when this happens).
  */
 static bool
-_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
-							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
-							   bool *qual_ok)
+_bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+					 FmgrInfo *orderproc, BTArrayKeyInfo *array, bool *qual_ok)
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
@@ -1006,14 +1090,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
-	Assert(arraysk->sk_attno == skey->sk_attno);
 	Assert(array->num_elems > 0);
-	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
-		   arraysk->sk_strategy == BTEqualStrategyNumber);
-	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
-		   skey->sk_strategy != BTEqualStrategyNumber);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
 
 	/*
 	 * _bt_binsrch_array_skey searches an array for the entry best matching a
@@ -1112,6 +1190,105 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	return true;
 }
 
+/*
+ * Preprocessing of skip (non-SAOP) array scan key, used to determine
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function.
+ *
+ * Unlike _bt_saoparray_shrink, we don't modify caller's array in-place.  Skip
+ * arrays work by procedurally generating their elements as needed, so we just
+ * store the inequality as the skip array's low_compare or high_compare.  The
+ * array's elements will be generated from the range of values that satisfies
+ * both low_compare and high_compare.
+ */
+static bool
+_bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
+					 bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Array's index attribute will be constrained by a strict operator/key.
+	 * Array must not "contain a NULL element" (i.e. the scan must not apply
+	 * "IS NULL" qual when it reaches the end of the index that stores NULLs).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Consider if we should treat caller's scalar scan key as the skip
+	 * array's high_compare or low_compare.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way the scan can always safely assume that
+	 * it's okay to use the input-opclass-only-type proc from so->orderProcs[]
+	 * (they can be cross-type with SAOP arrays, but never with skip arrays).
+	 *
+	 * This approach is enabled by MINVAL/MAXVAL sentinel key markings, which
+	 * can be thought of as representing either the lowest or highest matching
+	 * array element (excluding the NULL element, where applicable, though as
+	 * just discussed it isn't applicable to this range skip array anyway).
+	 * Array keys marked MINVAL/MAXVAL never have a valid datum in their
+	 * sk_argument field.  The scan directly applies the array's low_compare
+	 * key when it encounters MINVAL in the array key proper (just as it
+	 * applies high_compare when it sees MAXVAL set in the array key proper).
+	 * The scan must never use the array's so->orderProcs[] proc against
+	 * low_compare's/high_compare's sk_argument, either (so->orderProcs[] is
+	 * only intended to be used with rhs datums from the array proper/index).
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* replace existing high_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing high_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's high_compare */
+			array->high_compare = skey;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* replace existing low_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing low_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's low_compare */
+			array->low_compare = skey;
+			break;
+		case BTEqualStrategyNumber:
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1137,6 +1314,12 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -1152,41 +1335,35 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	Oid			skip_eq_ops[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
-	ScanKey		cur;
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys we'll need to generate below
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -1201,18 +1378,20 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
+		ScanKey		inkey = scan->keyData + input_ikey,
+					cur;
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
 		Oid			elemtype;
@@ -1225,21 +1404,113 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		Datum	   *elem_values;
 		bool	   *elem_nulls;
 		int			num_nonnulls;
-		int			j;
+
+		/* set up next output scan key */
+		cur = &arrayKeyData[numArrayKeyData];
+
+		/* Backfill skip arrays for attrs < or <= input key's attr? */
+		while (numSkipArrayKeys && attno_skip <= inkey->sk_attno)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skip_eq_ops[attno_skip - 1];
+			CompactAttribute *attr;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/*
+				 * Attribute already has an = input key, so don't output a
+				 * skip array for attno_skip.  Just copy attribute's = input
+				 * key into arrayKeyData[] once outside this inner loop.
+				 *
+				 * Note: When we get here there must be a later attribute that
+				 * lacks an equality input key, and still needs a skip array
+				 * (if there wasn't then numSkipArrayKeys would be 0 by now).
+				 */
+				Assert(attno_skip == inkey->sk_attno);
+				/* inkey can't be last input key to be marked required: */
+				Assert(input_ikey < scan->numberOfKeys - 1);
+#if 0
+				/* Could be a redundant input scan key, so can't do this: */
+				Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+					   (inkey->sk_flags & SK_SEARCHNULL));
+#endif
+
+				attno_skip++;
+				break;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize generic BTArrayKeyInfo fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+
+			/* Initialize skip array specific BTArrayKeyInfo fields */
+			attr = TupleDescCompactAttr(RelationGetDescr(rel), attno_skip - 1);
+			reverse = (indoption[attno_skip - 1] & INDOPTION_DESC) != 0;
+			so->arrayKeys[numArrayKeys].attlen = attr->attlen;
+			so->arrayKeys[numArrayKeys].attbyval = attr->attbyval;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup =
+				PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse);
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * We'll need a 3-way ORDER proc.  Set that up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			numArrayKeys++;
+			numArrayKeyData++;	/* keep this scan key/array */
+
+			/* set up next output scan key */
+			cur = &arrayKeyData[numArrayKeyData];
+
+			/* remember having output this skip array and scan key */
+			numSkipArrayKeys--;
+			attno_skip++;
+		}
 
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
-		*cur = scan->keyData[input_ikey];
+		*cur = *inkey;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -1257,7 +1528,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * all btree operators are strict.
 		 */
 		num_nonnulls = 0;
-		for (j = 0; j < num_elems; j++)
+		for (int j = 0; j < num_elems; j++)
 		{
 			if (!elem_nulls[j])
 				elem_values[num_nonnulls++] = elem_values[j];
@@ -1295,7 +1566,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -1306,7 +1577,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -1323,7 +1594,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -1392,23 +1663,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			origelemtype = elemtype;
 		}
 
-		/*
-		 * And set up the BTArrayKeyInfo data.
-		 *
-		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
-		 * scan_key field later on, after so->keyData[] has been finalized.
-		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		/* Initialize generic BTArrayKeyInfo fields */
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
+
+		/* Initialize SAOP array specific BTArrayKeyInfo fields */
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].cur_elem = -1;	/* i.e. invalid */
+
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
+	Assert(numSkipArrayKeys == 0);
+
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -1514,7 +1786,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -1575,6 +1848,189 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit every so->arrayKeys[] entry.
+ *
+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller must add this many
+ * skip arrays to each of the most significant attributes lacking any keys
+ * that use the = strategy (IS NULL keys count as = keys here).  The specific
+ * attributes that need skip arrays are indicated by initializing caller's
+ * skip_eq_ops[] 0-based attribute offset to a valid = op strategy Oid.  We'll
+ * only ever set skip_eq_ops[] entries to InvalidOid for attributes that
+ * already have an equality key in scan->keyData[] input keys -- and only when
+ * there's some later "attribute gap" for us to "fill-in" with a skip array.
+ *
+ * We're optimistic about skipping working out: we always add exactly the skip
+ * arrays needed to maximize the number of input scan keys that can ultimately
+ * be marked as required to continue the scan (but no more).  For a composite
+ * index on (a, b, c, d), we'll instruct caller to add skip arrays as follows:
+ *
+ * Input keys						Output keys (after all preprocessing)
+ * ----------						-------------------------------------
+ * a = 1							a = 1 (no skip arrays)
+ * a = ANY('{0, 1}')				a = ANY('{0, 1}') (no skip arrays)
+ * b = 42							skip a AND b = 42
+ * b = ANY('{40, 42}')				skip a AND b = ANY('{40, 42}')
+ * a = 1 AND b = 42					a = 1 AND b = 42 (no skip arrays)
+ * a >= 1 AND b = 42				range skip a AND b = 42
+ * a = 1 AND b > 42					a = 1 AND b > 42 (no skip arrays)
+ * a >= 1 AND a <= 3 AND b = 42		range skip a AND b = 42
+ * a = 1 AND c <= 27				a = 1 AND skip b AND c <= 27
+ * a = 1 AND d >= 1					a = 1 AND skip b AND skip c AND d >= 1
+ * a = 1 AND b >= 42 AND d > 1		a = 1 AND range skip b AND skip c AND d > 1
+ */
+static int
+_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_skip = 1,
+				attno_inkey = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+#ifdef DEBUG_DISABLE_SKIP_SCAN
+	/* don't attempt to add skip arrays */
+	return numArrayKeys;
+#endif
+
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+			/* Look up input opclass's equality operator (might fail) */
+			skip_eq_ops[attno_skip - 1] =
+				get_opfamily_member(opfamily, opcintype, opcintype,
+									BTEqualStrategyNumber);
+			if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip attribute for the attribute of the last input scan key
+		 * (doesn't matter whether that attribute has an equality key, or just
+		 * uses inequality keys).
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys
+			 */
+			if (attno_has_equal)
+			{
+				/* Attributes with an = key must have InvalidOid eq_op set */
+				skip_eq_ops[attno_skip - 1] = InvalidOid;
+			}
+			else
+			{
+				Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+				Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+				/* Look up input opclass's equality operator (might fail) */
+				skip_eq_ops[attno_skip - 1] =
+					get_opfamily_member(opfamily, opcintype, opcintype,
+										BTEqualStrategyNumber);
+
+				if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+				{
+					/*
+					 * Input opclass lacks an equality strategy operator, so
+					 * don't generate a skip array that definitely won't work
+					 */
+					break;
+				}
+
+				/* plan on adding a backfill skip array for this attribute */
+				(*numSkipArrayKeys)++;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys include any equality strategy
+		 * scan keys (IS NULL keys count as equality keys here)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required later on.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
 /*
  * _bt_find_extreme_element() -- get least or greatest array element
  *
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 80b04d6ca..815cbcfb7 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -31,6 +31,7 @@
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
 #include "storage/read_stream.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -76,14 +77,26 @@ typedef struct BTParallelScanDescData
 
 	/*
 	 * btps_arrElems is used when scans need to schedule another primitive
-	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
+	 * index scan with one or more SAOP arrays.  Holds BTArrayKeyInfo.cur_elem
+	 * offsets for each = scan key associated with a ScalarArrayOp array.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * Additional space (at the end of the struct) is used when scans need to
+	 * schedule another primitive index scan with one or more skip arrays.
+	 * Holds a flattened datum representation for each = scan key associated
+	 * with a skip array.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -541,10 +554,166 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume that every input scan key will be output with
+	 * its own SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * array (and an associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		CompactAttribute *attr;
+
+		/*
+		 * We make the conservative assumption that every index column will
+		 * also require a skip array.
+		 *
+		 * Every skip array must have space to store its scan key's sk_flags.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		/* Consider space required to store a datum of opclass input type */
+		attr = TupleDescCompactAttr(rel->rd_att, attnum - 1);
+		if (attr->attbyval)
+		{
+			/* This index attribute stores pass-by-value datums */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  true, attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * This index attribute stores pass-by-reference datums.
+		 *
+		 * Assume that serializing this array will use just as much space as a
+		 * pass-by-value datum, in addition to space for the largest possible
+		 * whole index tuple (this is not just a per-datum portion of the
+		 * largest possible tuple because that'd be almost as large anyway).
+		 *
+		 * This is quite conservative, but it's not clear how we could do much
+		 * better.  The executor requires an up-front storage request size
+		 * that reliably covers the scan's high watermark memory usage.  We
+		 * can't be sure of the real high watermark until the scan is over.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BTMaxItemSize);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   array->attbyval, array->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array key, while freeing memory allocated for old
+		 * sk_argument where required
+		 */
+		if (!array->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -613,6 +782,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -679,14 +849,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -831,6 +996,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -849,12 +1015,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
 	LWLockRelease(&btscan->btps_lock);
 }
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 3d46fb5df..2524cb23a 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -965,6 +965,15 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * attributes to its right, because it would break our simplistic notion
 	 * of what initial positioning strategy to use.
 	 *
+	 * In practice we rarely see any attributes with no boundary key here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  All
+	 * array keys are always considered = keys, but we'll sometimes need to
+	 * treat the current key value as if we were using an inequality strategy.
+	 * This happens whenever a skip array's scan key is found to have been set
+	 * to one of the special sentinel values representing an imaginary point
+	 * before/after some (or all) of the index's real indexable values.
+	 *
 	 * When the scan keys include cross-type operators, _bt_preprocess_keys
 	 * may not be able to eliminate redundant keys; in such cases we will
 	 * arbitrarily pick a usable one for each attribute.  This is correct
@@ -1040,8 +1049,54 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is MINVAL, choose low_compare (when scanning
+				 * backwards it'll be MAXVAL, and we'll choose high_compare).
+				 *
+				 * Note: if the array's low_compare key makes 'chosen' NULL,
+				 * then we behave as if the array's first element is -inf,
+				 * except when !array->null_elem implies a usable NOT NULL
+				 * constraint.
+				 */
+				if (chosen != NULL &&
+					(chosen->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skipequalitykey = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MAXVAL));
+						chosen = array->low_compare;
+					}
+					else
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MINVAL));
+						chosen = array->high_compare;
+					}
+
+					Assert(chosen == NULL ||
+						   chosen->sk_attno == skipequalitykey->sk_attno);
+
+					if (!array->null_elem)
+						impliesNN = skipequalitykey;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1084,9 +1139,40 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 
 				/*
-				 * Done if that was the last attribute, or if next key is not
-				 * in sequence (implying no boundary key is available for the
-				 * next attribute).
+				 * If the key that we just added to startKeys[] is a skip
+				 * array = key whose current element is marked NEXT or PRIOR,
+				 * make strat_total > or < (and stop adding boundary keys).
+				 * This can only happen with opclasses that lack skip support.
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(strat_total == BTEqualStrategyNumber);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We're done.  We'll never find an exact = match for a
+					 * NEXT or PRIOR sentinel sk_argument value.  There's no
+					 * sense in trying to add more keys to startKeys[].
+					 */
+					break;
+				}
+
+				/*
+				 * Done if that was the last scan key output by preprocessing.
+				 * Also done if there is a gap index attribute that lacks a
+				 * usable key (only possible when preprocessing was unable to
+				 * generate a skip array key to "fill in the gap").
 				 */
 				if (i >= so->numberOfKeys ||
 					cur->sk_attno != curattr + 1)
@@ -1581,31 +1667,10 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * We skip this for the first page read by each (primitive) scan, to avoid
 	 * slowing down point queries.  They typically don't stand to gain much
 	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
-	 *
-	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
-	 * just set a low-order required array's key to the best available match
-	 * for a truncated -inf attribute value from the prior page's high key
-	 * (array element 0 is always the best available match in this scenario).
-	 * It's quite likely that matches for array element 0 begin on this page,
-	 * but the start of matches won't necessarily align with page boundaries.
-	 * When the start of matches is somewhere in the middle of this page, it
-	 * would be wrong to treat page's final non-pivot tuple as representative.
-	 * Doing so might lead us to treat some of the page's earlier tuples as
-	 * being part of a group of tuples thought to satisfy the required keys.
-	 *
-	 * Note: Conversely, in the case where the scan's arrays just advanced
-	 * using the prior page's HIKEY _without_ advancement setting scanBehind,
-	 * the start of matches must be aligned with page boundaries, which makes
-	 * it safe to attempt the optimization here now.  It's also safe when the
-	 * prior page's HIKEY simply didn't need to advance any required array. In
-	 * both cases we can safely assume that the _first_ tuple from this page
-	 * must be >= the current set of array keys/equality constraints. And so
-	 * if the final tuple is == those same keys (and also satisfies any
-	 * required < or <= strategy scan keys) during the precheck, we can safely
-	 * assume that this must also be true of all earlier tuples from the page.
+	 * overhead of the precheck.  Also avoid it during scans with array keys,
+	 * which might be using skip scan (XXX fixed in next commit).
 	 */
-	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !arrayKeys && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 2aee9bbf6..62530702f 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -30,6 +30,17 @@
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									  int32 set_elem_result, Datum tupdatum, bool tupnull);
+static void _bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_array_set_low_or_high(Relation rel, ScanKey skey,
+									  BTArrayKeyInfo *array, bool low_not_high);
+static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -207,6 +218,7 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 	int32		result = 0;
 
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
+	Assert(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)));
 
 	if (tupnull)				/* NULL tupdatum */
 	{
@@ -283,6 +295,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* SAOP arrays never have NULLs */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -405,6 +419,185 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, since skip arrays don't really
+ * contain elements (they generate their array elements procedurally instead).
+ * Our interface matches that of _bt_binsrch_array_skey in every other way.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  We return -1 when tupdatum/tupnull is lower that any value
+ * within the range of the array, and 1 when it is higher than every value.
+ * Caller should pass *set_elem_result to _bt_skiparray_set_element to advance
+ * the array.
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (array->null_elem)
+	{
+		Assert(!array->low_compare && !array->high_compare);
+
+		*set_elem_result = 0;
+		return;
+	}
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+
+	/*
+	 * Assert that any keys that were assumed to be satisfied already (due to
+	 * caller passing cur_elem_trig=true) really are satisfied as expected
+	 */
+#ifdef USE_ASSERT_CHECKING
+	if (cur_elem_trig)
+	{
+		if (ScanDirectionIsForward(dir) && array->low_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												  array->low_compare->sk_collation,
+												  tupdatum,
+												  array->low_compare->sk_argument)));
+
+		if (ScanDirectionIsBackward(dir) && array->high_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												  array->high_compare->sk_collation,
+												  tupdatum,
+												  array->high_compare->sk_argument)));
+	}
+#endif
+}
+
+/*
+ * _bt_skiparray_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Caller passes set_elem_result returned by _bt_binsrch_skiparray_skey for
+ * caller's tupdatum/tupnull.
+ *
+ * We copy tupdatum/tupnull into skey's sk_argument iff set_elem_result == 0.
+ * Otherwise, we set skey to either the lowest or highest value that's within
+ * the range of caller's skip array (whichever is the best available match to
+ * tupdatum/tupnull that is still within the range of the skip array according
+ * to _bt_binsrch_skiparray_skey/set_elem_result).
+ */
+static void
+_bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  int32 set_elem_result, Datum tupdatum, bool tupnull)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (set_elem_result)
+	{
+		/* tupdatum/tupnull is out of the range of the skip array */
+		Assert(!array->null_elem);
+
+		_bt_array_set_low_or_high(rel, skey, array, set_elem_result < 0);
+		return;
+	}
+
+	/* Advance skip array to tupdatum (or tupnull) value */
+	if (unlikely(tupnull))
+	{
+		_bt_skiparray_set_isnull(rel, skey, array);
+		return;
+	}
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen);
+}
+
+/*
+ * _bt_skiparray_set_isnull() -- set skip array scan key to NULL
+ */
+static void
+_bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->null_elem && !array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -414,29 +607,355 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_array_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_array_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  bool low_not_high)
+{
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting array to lowest element (according to low_compare) */
+		skey->sk_flags |= SK_BT_MINVAL;
+	}
+	else
+	{
+		/* Setting array to highest element (according to high_compare) */
+		skey->sk_flags |= SK_BT_MAXVAL;
+	}
+}
+
+/*
+ * _bt_array_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the minimum value within the range
+	 * of a skip array (often just -inf) is never decrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		/* Successfully "decremented" array */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly decrement sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Decrement" from NULL to the high_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->high_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup->decrement(rel, skey->sk_argument, &uflow);
+	if (unlikely(uflow))
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			_bt_skiparray_set_isnull(rel, skey, array);
+
+			/* Successfully "decremented" array to NULL */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	/* Successfully decremented array */
+	return true;
+}
+
+/*
+ * _bt_array_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MINVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the maximum value within the range
+	 * of a skip array (often just +inf) is never incrementable
+	 */
+	if (skey->sk_flags & SK_BT_MAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		/* Successfully "incremented" array */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly increment sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Increment" from NULL to the low_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->low_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup->increment(rel, skey->sk_argument, &oflow);
+	if (unlikely(oflow))
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			_bt_skiparray_set_isnull(rel, skey, array);
+
+			/* Successfully "incremented" array to NULL */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	/* Successfully incremented array */
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -452,6 +971,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -461,29 +981,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_array_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_array_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -507,7 +1028,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 }
 
 /*
- * _bt_rewind_nonrequired_arrays() -- Rewind non-required arrays
+ * _bt_rewind_nonrequired_arrays() -- Rewind SAOP arrays not marked required
  *
  * Called when _bt_advance_array_keys decides to start a new primitive index
  * scan on the basis of the current scan position being before the position
@@ -539,10 +1060,15 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
  *
  * Note: _bt_verify_arrays_bt_first is called by an assertion to enforce that
  * everybody got this right.
+ *
+ * Note: In practice almost all SAOP arrays are marked required during
+ * preprocessing (if necessary by generating skip arrays).  It is hardly ever
+ * truly necessary to call here, but consistently doing so is simpler.
  */
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -550,7 +1076,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -562,16 +1087,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_array_set_low_or_high(rel, cur, array,
+								  ScanDirectionIsForward(dir));
 	}
 }
 
@@ -696,9 +1215,77 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (likely(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))))
+		{
+			/* Scankey has a valid/comparable sk_argument value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0)
+			{
+				/*
+				 * Interpret result in a way that takes NEXT/PRIOR into
+				 * account
+				 */
+				if (cur->sk_flags & SK_BT_NEXT)
+					result = -1;
+				else if (cur->sk_flags & SK_BT_PRIOR)
+					result = 1;
+
+				Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP));
+			}
+		}
+		else
+		{
+			BTArrayKeyInfo *array = NULL;
+
+			/*
+			 * Current array element/array = scan key value is a sentinel
+			 * value that represents the lowest (or highest) possible value
+			 * that's still within the range of the array.
+			 *
+			 * Like _bt_first, we only see MINVAL keys during forwards scans
+			 * (and similarly only see MAXVAL keys during backwards scans).
+			 * Even if the scan's direction changes, we'll stop at some higher
+			 * order key before we can ever reach any MAXVAL (or MINVAL) keys.
+			 * (However, unlike _bt_first we _can_ get to keys marked either
+			 * NEXT or PRIOR, regardless of the scan's current direction.)
+			 */
+			Assert(ScanDirectionIsForward(dir) ?
+				   !(cur->sk_flags & SK_BT_MAXVAL) :
+				   !(cur->sk_flags & SK_BT_MINVAL));
+
+			/*
+			 * There are no valid sk_argument values in MINVAL/MAXVAL keys.
+			 * Check if tupdatum is within the range of skip array instead.
+			 */
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			_bt_binsrch_skiparray_skey(false, dir, tupdatum, tupnull,
+									   array, cur, &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum satisfies both low_compare and high_compare, so
+				 * it's time to advance the array keys.
+				 *
+				 * Note: It's possible that the skip array will "advance" from
+				 * its MINVAL (or MAXVAL) representation to an alternative,
+				 * logically equivalent representation of the same value: a
+				 * representation where the = key gets a valid datum in its
+				 * sk_argument.  This is only possible when low_compare uses
+				 * the >= strategy (or high_compare uses the <= strategy).
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1017,18 +1604,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1053,18 +1631,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -1080,14 +1649,22 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			bool		cur_elem_trig = (sktrig_required && ikey == sktrig);
 
 			/*
-			 * Binary search for closest match that's available from the array
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems == -1)
+				_bt_binsrch_skiparray_skey(cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * Binary search for the closest match from the SAOP array
+			 */
+			else
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 		}
 		else
 		{
@@ -1163,11 +1740,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+		if (array)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->num_elems == -1)
+			{
+				/* Skip array's new element is tupdatum (or MINVAL/MAXVAL) */
+				_bt_skiparray_set_element(rel, cur, array, result,
+										  tupdatum, tupnull);
+			}
+			else if (array->cur_elem != set_elem)
+			{
+				/* SAOP array's new element is set_elem datum */
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
 		}
 	}
 
@@ -1581,10 +2168,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -1914,6 +2502,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key uses one of several sentinel values.  We just
+		 * fall back on _bt_tuple_before_array_skeys when we see such a value.
+		 */
+		if (key->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL |
+							 SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index dd6f5a15c..817ad358f 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -106,6 +106,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 8546366ee..a6dd8eab5 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("ordering equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (GetIndexAmRoutineByAmId(amoid, false)->amcanhash)
 	{
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index f279853de..4227ab1a7 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f23cfad71..244f48f4f 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 5b35debc8..bb46ed0c7 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -193,6 +193,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -214,6 +216,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5943,6 +5947,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -7001,6 +7091,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -7010,17 +7147,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
+	List	   *indexSkipQuals;
 	int			indexcol;
 	bool		eqQualHere;
-	bool		found_saop;
+	bool		found_row_compare;
+	bool		found_array;
 	bool		found_is_null_op;
+	bool		have_correlation = false;
 	double		num_sa_scans;
+	double		correlation = 0.0;
 	ListCell   *lc;
 
 	/*
@@ -7031,19 +7170,24 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * it's OK to count them in indexSelectivity, but they should not count
 	 * for estimating numIndexTuples.  So we must examine the given indexquals
 	 * to find out which ones count as boundary quals.  We rely on the
-	 * knowledge that they are given in index column order.
+	 * knowledge that they are given in index column order.  Note that nbtree
+	 * preprocessing can add skip arrays that act as leading '=' quals in the
+	 * absence of ordinary input '=' quals, so in practice _most_ input quals
+	 * are able to act as index bound quals (which we take into account here).
 	 *
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
+	 * If there's a SAOP or skip array in the quals, we'll actually perform up
+	 * to N index descents (not just one), but the underlying array key's
 	 * operator can be considered to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
+	indexSkipQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
-	found_saop = false;
+	found_row_compare = false;
+	found_array = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -7051,17 +7195,202 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
 		ListCell   *lc2;
 
-		if (indexcol != iclause->indexcol)
+		if (indexcol < iclause->indexcol)
 		{
-			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			double		num_sa_scans_prev_cols = num_sa_scans;
+
+			/*
+			 * Beginning of a new column's quals.
+			 *
+			 * Skip scans use skip arrays, which are ScalarArrayOp style
+			 * arrays that generate their elements procedurally and on demand.
+			 * Given a composite index on "(a, b)", and an SQL WHERE clause
+			 * "WHERE b = 42", a skip scan will effectively use an indexqual
+			 * "WHERE a = ANY('{every col a value}') AND b = 42".  (Obviously,
+			 * the array on "a" must also return "IS NULL" matches, since our
+			 * WHERE clause used no strict operator on "a").
+			 *
+			 * Here we consider how nbtree will backfill skip arrays for any
+			 * index columns that lacked an '=' qual.  This maintains our
+			 * num_sa_scans estimate, and determines if this new column (the
+			 * "iclause->indexcol" column, not the prior "indexcol" column)
+			 * can have its RestrictInfos/quals added to indexBoundQuals.
+			 *
+			 * We'll need to handle columns that have inequality quals, where
+			 * the skip array generates values from a range constrained by the
+			 * quals (not every possible value, never with IS NULL matching).
+			 * indexSkipQuals tracks the prior column's quals (that is, the
+			 * "indexcol" column's quals) to help us with this.
+			 */
+			if (found_row_compare)
+			{
+				/*
+				 * Skip arrays can't be added after a RowCompare input qual
+				 * due to limitations in nbtree
+				 */
+				break;
+			}
+			if (eqQualHere)
+			{
+				/*
+				 * Don't need to add a skip array for an indexcol that already
+				 * has an '=' qual/equality constraint
+				 */
+				indexcol++;
+				indexSkipQuals = NIL;
+			}
 			eqQualHere = false;
-			indexcol++;
+
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct;
+				bool		isdefault = true;
+
+				found_array = true;
+
+				/*
+				 * A skipped attribute's ndistinct forms the basis of our
+				 * estimate of the total number of "array elements" used by
+				 * its skip array at runtime.  Look that up first.
+				 */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+				if (indexcol == 0)
+				{
+					/*
+					 * Get an estimate of the leading column's correlation in
+					 * passing (avoids rereading variable stats below)
+					 */
+					if (HeapTupleIsValid(vardata.statsTuple))
+						correlation = btcost_correlation(index, &vardata);
+					have_correlation = true;
+				}
+
+				ReleaseVariableStats(vardata);
+
+				/*
+				 * If ndistinct is a default estimate, conservatively assume
+				 * that no skipping will happen at runtime
+				 */
+				if (isdefault)
+				{
+					num_sa_scans = num_sa_scans_prev_cols;
+					break;		/* done building indexBoundQuals */
+				}
+
+				/*
+				 * Apply indexcol's indexSkipQuals selectivity to ndistinct
+				 */
+				if (indexSkipQuals != NIL)
+				{
+					List	   *partialSkipQuals;
+					Selectivity ndistinctfrac;
+
+					/*
+					 * If the index is partial, AND the index predicate with
+					 * the index-bound quals to produce a more accurate idea
+					 * of the number of distinct values for prior indexcol
+					 */
+					partialSkipQuals = add_predicate_to_index_quals(index,
+																	indexSkipQuals);
+
+					ndistinctfrac = clauselist_selectivity(root, partialSkipQuals,
+														   index->rel->relid,
+														   JOIN_INNER,
+														   NULL);
+
+					/*
+					 * If ndistinctfrac is selective (on its own), the scan is
+					 * unlikely to benefit from repositioning itself using
+					 * later quals.  Do not allow iclause->indexcol's quals to
+					 * be added to indexBoundQuals (it would increase descent
+					 * costs, without lowering numIndexTuples costs by much).
+					 */
+					if (ndistinctfrac < DEFAULT_RANGE_INEQ_SEL)
+					{
+						num_sa_scans = num_sa_scans_prev_cols;
+						break;	/* done building indexBoundQuals */
+					}
+
+					/* Adjust ndistinct downward */
+					ndistinct = rint(ndistinct * ndistinctfrac);
+					ndistinct = Max(ndistinct, 1);
+				}
+
+				/*
+				 * When there's no inequality quals, account for the need to
+				 * find an initial value by counting -inf/+inf as a value.
+				 *
+				 * We don't charge anything extra for possible next/prior key
+				 * index probes, which are sometimes used to find the next
+				 * valid skip array element (ahead of using the located
+				 * element value to relocate the scan to the next position
+				 * that might contain matching tuples).  It seems hard to do
+				 * better here.  Use of the skip support infrastructure often
+				 * avoids most next/prior key probes.  But even when it can't,
+				 * there's a decent chance that most individual next/prior key
+				 * probes will locate a leaf page whose key space overlaps all
+				 * of the scan's keys (even the lower-order keys) -- which
+				 * also avoids the need for a separate, extra index descent.
+				 * Note also that these probes are much cheaper than non-probe
+				 * primitive index scans: they're reliably very selective.
+				 */
+				if (indexSkipQuals == NIL)
+					ndistinct += 1;
+
+				/*
+				 * Update num_sa_scans estimate by multiplying by ndistinct.
+				 *
+				 * We make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * expecting skipping to be helpful...
+				 */
+				num_sa_scans *= ndistinct;
+
+				/*
+				 * ...but back out of adding this latest group of 1 or more
+				 * skip arrays when num_sa_scans exceeds the total number of
+				 * index pages (revert to num_sa_scans from before indexcol).
+				 * This causes a sharp discontinuity in cost (as a function of
+				 * the indexcol's ndistinct), but that is representative of
+				 * actual runtime costs.
+				 *
+				 * Note that skipping is helpful when each primitive index
+				 * scan only manages to skip over 1 or 2 irrelevant leaf pages
+				 * on average.  Skip arrays bring savings in CPU costs due to
+				 * the scan not needing to evaluate indexquals against every
+				 * tuple, which can greatly exceed any savings in I/O costs.
+				 * This test is a test of whether num_sa_scans implies that
+				 * we're past the point where the ability to skip ceases to
+				 * lower the scan's costs (even qual evaluation CPU costs).
+				 */
+				if (index->pages < num_sa_scans)
+				{
+					num_sa_scans = num_sa_scans_prev_cols;
+					break;		/* done building indexBoundQuals */
+				}
+
+				indexcol++;
+				indexSkipQuals = NIL;
+			}
+
+			/*
+			 * Finished considering the need to add skip arrays to bridge an
+			 * initial eqQualHere gap between the old and new index columns
+			 * (or there was no initial eqQualHere gap in the first place).
+			 *
+			 * If an initial gap could not be bridged, then new column's quals
+			 * (i.e. iclause->indexcol's quals) won't go into indexBoundQuals,
+			 * and so won't affect our final numIndexTuples estimate.
+			 */
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+				break;			/* done building indexBoundQuals */
 		}
 
+		Assert(indexcol == iclause->indexcol);
+
 		/* Examine each indexqual associated with this index clause */
 		foreach(lc2, iclause->indexquals)
 		{
@@ -7081,6 +7410,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_row_compare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -7089,7 +7419,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				double		alength = estimate_array_length(root, other_operand);
 
 				clause_op = saop->opno;
-				found_saop = true;
+				found_array = true;
 				/* estimate SA descents by indexBoundQuals only */
 				if (alength > 1)
 					num_sa_scans *= alength;
@@ -7101,7 +7431,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skip scan purposes */
 					eqQualHere = true;
 				}
 			}
@@ -7120,19 +7450,28 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
+
+			/*
+			 * We apply inequality selectivities to estimate index descent
+			 * costs with scans that use skip arrays.  Save this indexcol's
+			 * RestrictInfos if it looks like they'll be needed for that.
+			 */
+			if (!eqQualHere && !found_row_compare &&
+				indexcol < index->nkeycolumns - 1)
+				indexSkipQuals = lappend(indexSkipQuals, rinfo);
 		}
 	}
 
 	/*
 	 * If index is unique and we found an '=' clause for each column, we can
 	 * just assume numIndexTuples = 1 and skip the expensive
-	 * clauselist_selectivity calculations.  However, a ScalarArrayOp or
-	 * NullTest invalidates that theory, even though it sets eqQualHere.
+	 * clauselist_selectivity calculations.  However, an array or NullTest
+	 * always invalidates that theory (even when eqQualHere has been set).
 	 */
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
-		!found_saop &&
+		!found_array &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
 	else
@@ -7154,7 +7493,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		numIndexTuples = btreeSelectivity * index->rel->tuples;
 
 		/*
-		 * btree automatically combines individual ScalarArrayOpExpr primitive
+		 * btree automatically combines individual array element primitive
 		 * index scans whenever the tuples covered by the next set of array
 		 * keys are close to tuples covered by the current set.  That puts a
 		 * natural ceiling on the worst case number of descents -- there
@@ -7172,16 +7511,18 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * of leaf pages (we make it 1/3 the total number of pages instead) to
 		 * give the btree code credit for its ability to continue on the leaf
 		 * level with low selectivity scans.
+		 *
+		 * Note: num_sa_scans includes both ScalarArrayOp array elements and
+		 * skip array elements whose qual affects our numIndexTuples estimate.
 		 */
 		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
-		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
-		 * to integer.
+		 * As in genericcostestimate(), we have to adjust for any array quals
+		 * included in indexBoundQuals, and then round to integer.
 		 *
-		 * It is tempting to make genericcostestimate behave as if SAOP
+		 * It is tempting to make genericcostestimate behave as if array
 		 * clauses work in almost the same way as scalar operators during
 		 * btree scans, making the top-level scan look like a continuous scan
 		 * (as opposed to num_sa_scans-many primitive index scans).  After
@@ -7214,7 +7555,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * comparisons to descend a btree of N leaf tuples.  We charge one
 	 * cpu_operator_cost per comparison.
 	 *
-	 * If there are ScalarArrayOpExprs, charge this once per estimated SA
+	 * If there are SAOP/skip array keys, charge this once per estimated SA
 	 * index descent.  The ones after the first one are not startup cost so
 	 * far as the overall plan goes, so just add them to "total" cost.
 	 */
@@ -7234,110 +7575,25 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * cost is somewhat arbitrarily set at 50x cpu_operator_cost per page
 	 * touched.  The number of such pages is btree tree height plus one (ie,
 	 * we charge for the leaf page too).  As above, charge once per estimated
-	 * SA index descent.
+	 * SAOP/skip array descent.
 	 */
 	descentCost = (index->tree_height + 1) * DEFAULT_PAGE_CPU_MULTIPLIER * cpu_operator_cost;
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* btcost_correlation already called earlier on */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..2bd35d2d2
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns skip support struct, allocating in caller's memory
+ * context.  Otherwise returns NULL, indicating that operator class has no
+ * skip support function.
+ */
+SkipSupport
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse)
+{
+	Oid			skipSupportFunction;
+	SkipSupport sksup;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return NULL;
+
+	sksup = palloc(sizeof(SkipSupportData));
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return sksup;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 9682f9dbd..347089b76 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2304,6 +2305,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 4f8402ef9..1b72059d2 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,6 +13,7 @@
 
 #include "postgres.h"
 
+#include <limits.h>
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
@@ -21,6 +22,7 @@
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -416,6 +418,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..3e6f30d74 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value that
+     might be stored in an index, so the domain of the particular data type
+     stored within the index (the input opclass type) must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index 768b77aa0..d5adb58c1 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -835,7 +835,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index aaa6586d3..e6a0b7093 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4249,7 +4249,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage
      <replaceable>column_name</replaceable> =
      <replaceable>value2</replaceable> ...</literal> construct, though only
     when the optimizer transforms the construct into an equivalent
-    multi-valued array representation.
+    multi-valued array representation.  Similarly, when B-Tree index scans use
+    the skip scan strategy, an index search is performed each time the scan is
+    repositioned to the next index leaf page that might have matching tuples.
    </para>
   </note>
   <tip>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index 387baac7e..d0470ac79 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -860,6 +860,37 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE thousand IN (1, 2, 3, 4);
     <structname>tenk1_thous_tenthous</structname> index leaf page.
    </para>
 
+   <para>
+    The <quote>Index Searches</quote> line is also useful with B-tree index
+    scans that apply the <firstterm>skip scan</firstterm> optimization to
+    more efficiently traverse through an index:
+<screen>
+EXPLAIN ANALYZE SELECT four, unique1 FROM tenk1 WHERE four BETWEEN 1 AND 3 AND unique1 = 42;
+                                                              QUERY PLAN
+-------------------------------------------------------------------&zwsp;---------------------------------------------------------------
+ Index Only Scan using tenk1_four_unique1_idx on tenk1  (cost=0.29..6.90 rows=1 width=8) (actual time=0.006..0.007 rows=1.00 loops=1)
+   Index Cond: ((four &gt;= 1) AND (four &lt;= 3) AND (unique1 = 42))
+   Heap Fetches: 0
+   Index Searches: 3
+   Buffers: shared hit=7
+ Planning Time: 0.029 ms
+ Execution Time: 0.012 ms
+</screen>
+
+    Here we see an Index-Only Scan node using
+    <structname>tenk1_four_unique1_idx</structname>, a composite index on the
+    <structname>tenk1</structname> table's <structfield>four</structfield> and
+    <structfield>unique1</structfield> columns.  The scan performs 3 searches
+    that each read a single index leaf page:
+    <quote><literal>four = 1 AND unique1 = 42</literal></quote>,
+    <quote><literal>four = 2 AND unique1 = 42</literal></quote>, and
+    <quote><literal>four = 3 AND unique1 = 42</literal></quote>.  This index
+    is generally a good target for skip scan, since its leading column (the
+    <structfield>four</structfield> column) contains only 4 distinct values,
+    while its second/final column (the <structfield>unique1</structfield>
+    column) contains many distinct values.
+   </para>
+
    <para>
     Another type of extra information is the number of rows removed by a
     filter condition:
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 053619624..7e23a7b6e 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index 0c274d56a..23bf33f10 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  ordering equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 8879554c3..bfb1a286e 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -581,6 +581,47 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Index Only Scan using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Index Only Scan Backward using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 --
 -- Test for multilevel page deletion
 --
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index bd5f002cf..15dde752f 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1637,7 +1637,9 @@ DROP TABLE syscol_table;
 -- Tests for IS NULL/IS NOT NULL with b-tree indexes
 --
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 SET enable_seqscan = OFF;
 SET enable_indexscan = ON;
@@ -1645,7 +1647,7 @@ SET enable_bitmapscan = ON;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1657,13 +1659,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1678,12 +1680,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1695,13 +1703,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1722,12 +1730,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0,
      1
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc nulls last,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1739,13 +1753,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1760,12 +1774,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2  nulls first,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1777,13 +1797,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1798,6 +1818,12 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 -- Check initial-positioning logic too
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2);
@@ -1829,20 +1855,24 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
 (2 rows)
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-         |        
-     278 |     999
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 5;
+ unique1 |  unique2   
+---------+------------
+     500 |           
+     100 |           
+         |           
+         | 2147483647
+     278 |        999
+(5 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-     278 |     999
-       0 |     998
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 3;
+ unique1 |  unique2   
+---------+------------
+         | 2147483647
+     278 |        999
+       0 |        998
+(3 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
@@ -2247,7 +2277,8 @@ SELECT count(*) FROM dupindexcols
 (1 row)
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 explain (costs off)
 SELECT unique1 FROM tenk1
@@ -2269,7 +2300,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2289,7 +2320,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2309,6 +2340,25 @@ ORDER BY thousand DESC, tenthous DESC;
         0 |     3000
 (2 rows)
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ Index Only Scan Backward using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > 995) AND (tenthous = ANY ('{998,999}'::integer[])))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+ thousand | tenthous 
+----------+----------
+      999 |      999
+      998 |      998
+(2 rows)
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -2339,6 +2389,45 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 ---------
 (0 rows)
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+                   QUERY PLAN                    
+-------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY (NULL::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY ('{NULL,NULL,NULL}'::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: ((unique1 IS NULL) AND (unique1 IS NULL))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+ unique1 
+---------
+(0 rows)
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
                                 QUERY PLAN                                 
@@ -2462,6 +2551,44 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 ---------
 (0 rows)
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > '-1'::integer) AND (thousand >= 0) AND (tenthous = 3000))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+(1 row)
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand < 3) AND (thousand <= 2) AND (tenthous = 1001))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        1 |     1001
+(1 row)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 8687ffe27..2c718e03f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5332,9 +5332,10 @@ Function              | in_range(time without time zone,time without time zone,i
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql
index 670ad5c6e..68c61dbc7 100644
--- a/src/test/regress/sql/btree_index.sql
+++ b/src/test/regress/sql/btree_index.sql
@@ -327,6 +327,27 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 
 --
 -- Test for multilevel page deletion
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index be570da08..40ba3d65b 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -650,7 +650,9 @@ DROP TABLE syscol_table;
 --
 
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 
 SET enable_seqscan = OFF;
@@ -663,6 +665,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -675,6 +678,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NUL
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0, 1);
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -686,6 +690,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -697,6 +702,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -716,9 +722,9 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
   ORDER BY unique2 LIMIT 2;
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 5;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 3;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
 
@@ -852,7 +858,8 @@ SELECT count(*) FROM dupindexcols
   WHERE f1 BETWEEN 'WA' AND 'ZZZ' and id < 1000 and f1 ~<~ 'YX';
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 
 explain (costs off)
@@ -864,7 +871,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -874,7 +881,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -884,6 +891,15 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand DESC, tenthous DESC;
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -897,6 +913,21 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 
 SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('{33, 44}'::bigint[]);
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
 
@@ -942,6 +973,26 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
 SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index bfa276d2d..977a3cebc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -223,6 +223,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2736,6 +2737,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.49.0

In reply to: Peter Geoghegan (#82)
4 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Sat, Mar 22, 2025 at 1:47 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v30, which fully replaces the pstate.prechecked
optimization with the new _bt_skip_ikeyprefix optimization (which now
appears in v30-0002-Lower-nbtree-skip-array-maintenance-overhead.patch,
and not in 0003-*, due to my committing the primscan scheduling patch
just now).

Attached is v31, which has a much-improved _bt_skip_ikeyprefix (which
I've once again renamed, this time to _bt_set_startikey).

There are some bug fixes here (nothing very interesting), and the
heuristics have been tuned to take into account the requirements of
conventional SAOP scans with dense/contiguous array keys (we should
mostly just be preserving existing performance characteristics here).

The newly expanded _bt_skip_ikeyprefix needs quite a bit more testing
and polishing to be committable. I didn't even update the relevant
commit message for v30. Plus I'm not completely sure what to do about
RowCompare keys just yet, which have some funny rules when dealing
with NULLs.

v31 fixes all these problems, too.

Most notably, v31 differs from v30 in that it removes *both* of the
optimizations added to Postgres 17 by Alexander Korotkov's commit
e0b1ee17 -- including the pstate.firstmatch optimization. I didn't
expect things to go this way. I recently said that pstate.firstmatch
is something that can and should be kept (and v30 only removed
pstate.prechecked). Obviously, I've since changed my mind.

I changed my mind because lots of things are noticeably faster this
way, across most of my microbenchmarks. These performance validation
tests/microbenchmarks are really sensitive to the number of branches
added to _bt_check_compare; removing anything nonessential from that
hot code path really matters here.

Notably, removing the pstate.firstmatch optimization (along with
removing pstate.prechecked) is enough to fully eliminate what I've
long considered to be the most important microbenchmark regressions. I
refer to the microbenchmark suite originally written by Masahiro
Ikeda, and later enhanced/expanded by me to use a wider variety of
data cardinalities and datatypes. For the last several months, I
thought we'd need to live with a 5% - 10% regression with such cases
(those were the numbers I've thrown around when giving a high-level
summary of the extent of the regressions in unfavorable cases). Now
these microbenchmarks show that the queries are all about ~2% *faster*
instead. What's more, there may even be a similar small improvement
for important standard benchmarks (not microbenchmarks), such as the
standard pgbench SELECT benchmark. (I think simple pgbench is that
much faster, which is enough to matter, but not enough that it's easy
to prove under time pressure.)

There is at least a theoretical downside to replacing
pstate.firstmatch with the new _bt_set_startikey path, that I must
acknowledge: we only actually call _bt_set_startikey on the second or
subsequent leaf page, so that's the earliest possible point that it
can help speed things up (exactly like pstate.prechecked). Whereas
pstate.firstmatch is usually effective right away, on the first page
(effective at allowing us to avoid evaluating > or >= keys in the
common case where we're scanning the index forwards). It's fair to
wonder how much we'd be missing out, by giving up on that advantage.
It's very difficult to actually see any benefit that can be tied to
that theoretical advantage for pstate.firstmatch, though.

What I actually see when I run my range microbenchmark suite with v31
(not the aforementioned main microbenchmark suite) is that simple
cases involving no skip arrays (only simple scalar inequalities), with
quals like "WHERE col BETWEEN 0 and 1_000_000" are now *also* about 2%
faster. Any slowdown is more than made up for. Granted, if I worked
hard enough I might find a counter-example, where it actually is
slower overall, but I just can't see that ever mattering enough to
make me reconsider getting rid of pstate.firstmatch.

--
Peter Geoghegan

Attachments:

v31-0002-Enhance-nbtree-tuple-scan-key-optimizations.patchapplication/octet-stream; name=v31-0002-Enhance-nbtree-tuple-scan-key-optimizations.patchDownload
From dceb5872f37553549923971419ae7b94c5b208ca Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v31 2/4] Enhance nbtree tuple scan key optimizations.

Postgres 17 commit e0b1ee17 added a pair of closely related nbtree
optimizations: the "prechecked" and "firstpage" optimizations.  Both
optimizations avoided needlessly evaluating keys that are guaranteed to
be satisfied by applying page-level context.  These optimizations were
adapted to work with the nbtree ScalarArrayOp execution patch a few
months later, which became commit 5bf748b8.

The "prechecked" design had a number of notable weak points.  It didn't
account for the fact that an = array scan key's sk_argument field might
need to advance at the point of the page precheck (it didn't check the
precheck tuple against the key's array, only the key's sk_argument,
which needlessly made it ineffective in corner cases involving stepping
to a page having advanced the scan's arrays using a truncated high key).
It was also an "all or nothing" optimization: either it was completely
effective (skipping all required-in-scan-direction keys against all
attributes) for the whole page, or it didn't work at all.  This also
implied that it couldn't be used on pages where the scan had to
terminate before reaching the end of the page due to an unsatisfied
low-order key setting continuescan=false.

Replace both optimizations with a new optimization without any of these
weak points.  This works by giving affected _bt_readpage calls a scankey
offset that its _bt_checkkeys calls start at (an offset to the first key
that might not be satisfied by every non-pivot tuple from the page).
The new optimization is activated at the same point as the previous
"prechecked" optimization (at the start of a _bt_readpage of any page
after the scan's first).

While the "prechecked" optimization worked off of the highest non-pivot
tuple on the page (or the lowest, when scanning backwards), the new
"startikey" optimization always works off of a pair of non-pivot tuples
(the lowest and the highest, taken together).  Using a pair of scan keys
like this allows the "startikey" optimization to work seamlessly with
all kinds of scan keys, including = array keys, as well as scalar
inequalities (required in either scan direction).

Although this is independently useful work, the main motivation is to
fix regressions in index scans that are nominally eligible to use skip
scan, but can never actually benefit from skipping.  These are cases
where a leading prefix column contains many distinct values, especially
when the number of values approaches the total number of index tuples,
where skipping can never be profitable.  The CPU costs of skip array
maintenance is by far the main cost that must be kept under control.

Some scans with array keys (including all scans with skip arrays) will
temporarily cease fully maintaining the scan's arrays when the
optimization kicks in.  _bt_checkkeys will treat the scan's keys as if
they were not marked as required during preprocessing, for the duration
of an affected _bt_readpage call.  This relies on the non-required array
logic that was added to Postgres 17 by commit 5bf748b8.

Skip scan's approach of adding skip arrays during preprocessing and then
fixing (or significantly ameliorating) the resulting regressions seen in
unsympathetic cases is enabled by the optimization added by this commit
(and by the "look ahead" optimization introduced by commit 5bf748b8).
This allows the planner to avoid generating distinct, competing index
paths (one path for skip scan, another for an equivalent traditional
full index scan).  The overall effect is to make scan runtime close to
optimal, even when the planner works off an incorrect cardinality
estimate.  Scans will also perform well given a skipped column with data
skew: individual groups of pages with many distinct values in respect of
a skipped column can be read about as efficiently as before, without
having to give up on skipping over other provably-irrelevant leaf pages.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WznWDK45JfNPNvDxh6RQy-TaCwULaM5u5ALMXbjLBMcugQ@mail.gmail.com
---
 src/include/access/nbtree.h                   |  11 +-
 src/backend/access/nbtree/nbtpreprocesskeys.c |   1 +
 src/backend/access/nbtree/nbtree.c            |   1 +
 src/backend/access/nbtree/nbtsearch.c         |  63 +-
 src/backend/access/nbtree/nbtutils.c          | 590 ++++++++++++++----
 5 files changed, 512 insertions(+), 154 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index b86bf7bf3..c8708f2fd 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1059,6 +1059,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Check scan not still behind on next page? */
 	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
@@ -1105,6 +1106,8 @@ typedef struct BTReadPageState
 	IndexTuple	finaltup;		/* Needed by scans with array keys */
 	Page		page;			/* Page being read */
 	bool		firstpage;		/* page is first for primitive scan? */
+	bool		forcenonrequired;	/* treat all keys as nonrequired? */
+	int			startikey;		/* start comparisons from this scan key */
 
 	/* Per-tuple input parameters, set by _bt_readpage for _bt_checkkeys */
 	OffsetNumber offnum;		/* current tuple's page offset number */
@@ -1113,13 +1116,6 @@ typedef struct BTReadPageState
 	OffsetNumber skip;			/* Array keys "look ahead" skip offnum */
 	bool		continuescan;	/* Terminate ongoing (primitive) index scan? */
 
-	/*
-	 * Input and output parameters, set and unset by both _bt_readpage and
-	 * _bt_checkkeys to manage precheck optimizations
-	 */
-	bool		prechecked;		/* precheck set continuescan to 'true'? */
-	bool		firstmatch;		/* at least one match so far?  */
-
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
 	 * (only used during scans with array keys)
@@ -1327,6 +1323,7 @@ extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arra
 						  IndexTuple tuple, int tupnatts);
 extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
 									 IndexTuple finaltup);
+extern void _bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 09cd9d751..5689e24b7 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -1353,6 +1353,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	 * (also checks if we should add extra skip arrays based on input keys)
 	 */
 	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
+	so->skipScan = (numSkipArrayKeys > 0);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 815cbcfb7..7d3ce6ecb 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -349,6 +349,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	else
 		so->keyData = NULL;
 
+	so->skipScan = false;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->oppositeDirCheck = false;
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 1ef2cb2b5..08cfbc45f 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1648,47 +1648,14 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.finaltup = NULL;
 	pstate.page = page;
 	pstate.firstpage = firstpage;
+	pstate.forcenonrequired = false;
+	pstate.startikey = 0;
 	pstate.offnum = InvalidOffsetNumber;
 	pstate.skip = InvalidOffsetNumber;
 	pstate.continuescan = true; /* default assumption */
-	pstate.prechecked = false;
-	pstate.firstmatch = false;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
-	/*
-	 * Prechecking the value of the continuescan flag for the last item on the
-	 * page (for backwards scan it will be the first item on a page).  If we
-	 * observe it to be true, then it should be true for all other items. This
-	 * allows us to do significant optimizations in the _bt_checkkeys()
-	 * function for all the items on the page.
-	 *
-	 * With the forward scan, we do this check for the last item on the page
-	 * instead of the high key.  It's relatively likely that the most
-	 * significant column in the high key will be different from the
-	 * corresponding value from the last item on the page.  So checking with
-	 * the last item on the page would give a more precise answer.
-	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.  Also avoid it during scans with array keys,
-	 * which might be using skip scan (XXX fixed in next commit).
-	 */
-	if (!pstate.firstpage && !arrayKeys && minoff < maxoff)
-	{
-		ItemId		iid;
-		IndexTuple	itup;
-
-		iid = PageGetItemId(page, ScanDirectionIsForward(dir) ? maxoff : minoff);
-		itup = (IndexTuple) PageGetItem(page, iid);
-
-		/* Call with arrayKeys=false to avoid undesirable side-effects */
-		_bt_checkkeys(scan, &pstate, false, itup, indnatts);
-		pstate.prechecked = pstate.continuescan;
-		pstate.continuescan = true; /* reset */
-	}
-
 	if (ScanDirectionIsForward(dir))
 	{
 		/* SK_SEARCHARRAY forward scans must provide high key up front */
@@ -1716,6 +1683,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
+		/*
+		 * Consider pstate.startikey optimization once the ongoing primitive
+		 * index scan has already read at least one page
+		 */
+		if (!pstate.firstpage && minoff < maxoff)
+			_bt_set_startikey(scan, &pstate);
+
 		/* load items[] in ascending order */
 		itemIndex = 0;
 
@@ -1752,6 +1726,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum < pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1761,7 +1736,6 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			if (passes_quals)
 			{
 				/* tuple passes all scan key conditions */
-				pstate.firstmatch = true;
 				if (!BTreeTupleIsPosting(itup))
 				{
 					/* Remember it */
@@ -1816,7 +1790,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			int			truncatt;
 
 			truncatt = BTreeTupleGetNAtts(itup, rel);
-			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
+			pstate.forcenonrequired = false;
+			pstate.startikey = 0;
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -1855,6 +1830,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
+		/*
+		 * Consider pstate.startikey optimization once the ongoing primitive
+		 * index scan has already read at least one page
+		 */
+		if (!pstate.firstpage && minoff < maxoff)
+			_bt_set_startikey(scan, &pstate);
+
 		/* load items[] in descending order */
 		itemIndex = MaxTIDsPerBTreePage;
 
@@ -1894,6 +1876,11 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff)
+			{
+				pstate.forcenonrequired = false;
+				pstate.startikey = 0;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
@@ -1905,6 +1892,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum > pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1914,7 +1902,6 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			if (passes_quals && tuple_alive)
 			{
 				/* tuple passes all scan key conditions */
-				pstate.firstmatch = true;
 				if (!BTreeTupleIsPosting(itup))
 				{
 					/* Remember it */
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 0e6b8b3ab..2dddaba7a 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -57,11 +57,11 @@ static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool forcenonrequired,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-								 ScanDirection dir, bool *continuescan);
+								 ScanDirection dir, bool forcenonrequired, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -1421,9 +1421,10 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
  * postcondition's <= operator with a >=.  In other words, just swap the
  * precondition with the postcondition.)
  *
- * We also deal with "advancing" non-required arrays here.  Callers whose
- * sktrig scan key is non-required specify sktrig_required=false.  These calls
- * are the only exception to the general rule about always advancing the
+ * We also deal with "advancing" non-required arrays here (or arrays that are
+ * treated as non-required for the duration of a _bt_readpage call).  Callers
+ * whose sktrig scan key is non-required specify sktrig_required=false.  These
+ * calls are the only exception to the general rule about always advancing the
  * required array keys (the scan may not even have a required array).  These
  * callers should just pass a NULL pstate (since there is never any question
  * of stopping the scan).  No call to _bt_tuple_before_array_skeys is required
@@ -1463,6 +1464,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 				all_satisfied = true;
 
 	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
+	Assert(_bt_verify_keys_with_arraykeys(scan));
 
 	if (sktrig_required)
 	{
@@ -1472,17 +1474,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 
-		/*
-		 * Required scan key wasn't satisfied, so required arrays will have to
-		 * advance.  Invalidate page-level state that tracks whether the
-		 * scan's required-in-opposite-direction-only keys are known to be
-		 * satisfied by page's remaining tuples.
-		 */
-		pstate->firstmatch = false;
-
-		/* Shouldn't have to invalidate 'prechecked', though */
-		Assert(!pstate->prechecked);
-
 		/*
 		 * Once we return we'll have a new set of required array keys, so
 		 * reset state used by "look ahead" optimization
@@ -1490,8 +1481,26 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		pstate->rechecks = 0;
 		pstate->targetdistance = 0;
 	}
+	else if (sktrig < so->numberOfKeys - 1 &&
+			 !(so->keyData[so->numberOfKeys - 1].sk_flags & SK_SEARCHARRAY))
+	{
+		int			least_sign_ikey = so->numberOfKeys - 1;
+		bool		continuescan;
 
-	Assert(_bt_verify_keys_with_arraykeys(scan));
+		/*
+		 * Optimization: perform a precheck of the least significant key
+		 * during !sktrig_required calls when it isn't already our sktrig
+		 * (provided the precheck key is not itself an array).
+		 *
+		 * When the precheck works out we'll avoid an expensive binary search
+		 * of sktrig's array (plus any other arrays before least_sign_ikey).
+		 */
+		Assert(so->keyData[sktrig].sk_flags & SK_SEARCHARRAY);
+		if (!_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false,
+							   false, &continuescan,
+							   &least_sign_ikey))
+			return false;
+	}
 
 	for (int ikey = 0; ikey < so->numberOfKeys; ikey++)
 	{
@@ -1533,8 +1542,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -1668,7 +1675,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -1710,7 +1717,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -1725,7 +1732,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -1785,6 +1792,12 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * of any required scan key).  All that matters is whether caller's tuple
 	 * satisfies the new qual, so it's safe to just skip the _bt_check_compare
 	 * recheck when we've already determined that it can only return 'false'.
+	 *
+	 * Note: In practice most scan keys are marked required by preprocessing,
+	 * if necessary by generating a preceding skip array.  We nevertheless
+	 * often handle array keys marked required as if they were nonrequired.
+	 * This behavior is requested by our _bt_check_compare caller, though only
+	 * when it is passed "forcenonrequired=true" by _bt_checkkeys.
 	 */
 	if ((sktrig_required && all_required_satisfied) ||
 		(!sktrig_required && all_satisfied))
@@ -1795,9 +1808,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		Assert(all_required_satisfied);
 
 		/* Recheck _bt_check_compare on behalf of caller */
-		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
-							  &continuescan, &nsktrig) &&
+		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false,
+							  false, &continuescan,
+							  &nsktrig) &&
 			!so->scanBehind)
 		{
 			/* This tuple satisfies the new qual */
@@ -2041,8 +2054,9 @@ new_prim_scan:
 	 * read at least one leaf page before the one we're reading now.  This
 	 * makes primscan scheduling more efficient when scanning subsets of an
 	 * index with many distinct attribute values matching many array elements.
-	 * It encourages fewer, larger primitive scans where that makes sense
-	 * (where index descent costs need to be kept under control).
+	 * It encourages fewer, larger primitive scans where that makes sense.
+	 * This will in turn encourage _bt_readpage to apply the pstate.startikey
+	 * optimization more often.
 	 *
 	 * Note: This heuristic isn't as aggressive as you might think.  We're
 	 * conservative about allowing a primitive scan to step from the first
@@ -2199,17 +2213,14 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
  * the page to the right.
  *
  * Advances the scan's array keys when necessary for arrayKeys=true callers.
- * Caller can avoid all array related side-effects when calling just to do a
- * page continuescan precheck -- pass arrayKeys=false for that.  Scans without
- * any arrays keys must always pass arrayKeys=false.
+ * Scans without any array keys must always pass arrayKeys=false.
  *
  * Also stops and starts primitive index scans for arrayKeys=true callers.
  * Scans with array keys are required to set up page state that helps us with
  * this.  The page's finaltup tuple (the page high key for a forward scan, or
  * the page's first non-pivot tuple for a backward scan) must be set in
- * pstate.finaltup ahead of the first call here for the page (or possibly the
- * first call after an initial continuescan-setting page precheck call).  Set
- * this to NULL for rightmost page (or the leftmost page for backwards scans).
+ * pstate.finaltup ahead of the first call here for the page.  Set this to
+ * NULL for rightmost page (or the leftmost page for backwards scans).
  *
  * scan: index scan descriptor (containing a search-type scankey)
  * pstate: page level input and output parameters
@@ -2224,42 +2235,48 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	TupleDesc	tupdesc = RelationGetDescr(scan->indexRelation);
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ScanDirection dir = so->currPos.dir;
-	int			ikey = 0;
+	int			ikey = pstate->startikey;
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
+	Assert(arrayKeys || so->numArrayKeys == 0);
 
-	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
-							&pstate->continuescan, &ikey);
+	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, arrayKeys,
+							pstate->forcenonrequired, &pstate->continuescan,
+							&ikey);
 
+	/*
+	 * If _bt_check_compare relied on the pstate.startikey optimization, call
+	 * again (in assert-enabled builds) to verify it didn't affect our answer.
+	 *
+	 * Note: we can't do this when !pstate.forcenonrequired, since any arrays
+	 * before pstate.startikey won't have advanced on this page at all.
+	 */
+	Assert(!pstate->forcenonrequired || arrayKeys);
 #ifdef USE_ASSERT_CHECKING
-	if (!arrayKeys && so->numArrayKeys)
+	if (pstate->startikey > 0 && !pstate->forcenonrequired)
 	{
-		/*
-		 * This is a continuescan precheck call for a scan with array keys.
-		 *
-		 * Assert that the scan isn't in danger of becoming confused.
-		 */
-		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
-		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
-											 tupnatts, false, 0, NULL));
-	}
-	if (pstate->prechecked || pstate->firstmatch)
-	{
-		bool		dcontinuescan;
+		bool		dres,
+					dcontinuescan;
 		int			dikey = 0;
 
-		/*
-		 * Call relied on continuescan/firstmatch prechecks -- assert that we
-		 * get the same answer without those optimizations
-		 */
-		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
-										&dcontinuescan, &dikey));
+		/* Pass arrayKeys=false to avoid array side-effects */
+		dres = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false,
+								 pstate->forcenonrequired, &dcontinuescan,
+								 &dikey);
+		Assert(res == dres);
 		Assert(pstate->continuescan == dcontinuescan);
+
+		/*
+		 * Should also get the same ikey result.  We need a slightly weaker
+		 * assertion during arrayKeys calls, since they might be using an
+		 * array that couldn't be marked required during preprocessing
+		 * (preprocessing occasionally fails to add a "bridging" skip array,
+		 * due to implementation restrictions around RowCompare keys).
+		 */
+		Assert(arrayKeys || ikey == dikey);
+		Assert(ikey <= dikey);
 	}
 #endif
 
@@ -2280,6 +2297,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * It's also possible that the scan is still _before_ the _start_ of
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
+	Assert(!pstate->forcenonrequired);
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
@@ -2393,8 +2411,9 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 
 	Assert(so->numArrayKeys);
 
-	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc, false,
+					  false, &continuescan,
+					  &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -2402,6 +2421,338 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	return true;
 }
 
+/*
+ * Determines an offset to the first scan key (an so->keyData[]-wise offset)
+ * that is _not_ guaranteed to be satisfied by every tuple from pstate.page,
+ * which is set in pstate.startikey for _bt_checkkeys calls for the page.
+ *
+ * Also determines if later calls to _bt_checkkeys (for pstate.page) should be
+ * forced to treat all required scan keys >= pstate.startikey as nonrequired
+ * (that is, if they're to be treated as if any SK_BT_REQFWD/SK_BT_REQBKWD
+ * markings that were set by preprocessing were not set at all, for the
+ * duration of _bt_checkkeys calls prior to the call for pstate.finaltup).
+ * This is indicated to caller by setting pstate.forcenonrequired.
+ *
+ * Call here at the start of reading a leaf page beyond the first one for the
+ * primitive index scan.  We consider all non-pivot tuples, so it doesn't make
+ * sense to call here when only a subset of those tuples can ever be read.
+ * This is also a good idea on performance grounds; not calling here when on
+ * the first page (first for the current primitive scan) avoids wasting cycles
+ * during selective point queries.  They typically don't stand to gain as much
+ * when we can set pstate.startikey, and are likely to notice the overhead of
+ * calling here.
+ *
+ * Caller must reset pstate.startikey and pstate.forcenonrequired just ahead
+ * of the _bt_checkkeys call for pstate.finaltup tuple.  _bt_checkkeys needs
+ * an opportunity to call _bt_advance_array_keys with sktrig_required=true, to
+ * advance the arrays that will have been ignored when checking prior tuples.
+ * Caller doesn't need to do this on the rightmost/leftmost page in the index
+ * (where pstate.finaltup won't ever be set), though.
+ */
+void
+_bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	ScanDirection dir = so->currPos.dir;
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	ItemId		iid;
+	IndexTuple	firsttup,
+				lasttup;
+	int			startikey = 0,
+				arrayidx = 0,
+				firstchangingattnum;
+	bool		start_past_saop_eq = false;
+
+	/* Should be checked and unset by the time we're called: */
+	Assert(!so->scanBehind);
+
+	/* Only call here when there's at least two non-pivot tuples: */
+	Assert(pstate->minoff < pstate->maxoff);
+	Assert(!pstate->firstpage);
+	Assert(pstate->startikey == 0);
+
+	if (so->numberOfKeys == 0)
+		return;
+
+	/* minoff is an offset to the lowest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->minoff);
+	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* maxoff is an offset to the highest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->maxoff);
+	lasttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* Determine the first attribute whose values change on caller's page */
+	firstchangingattnum = _bt_keep_natts_fast(rel, firsttup, lasttup);
+
+	for (; startikey < so->numberOfKeys; startikey++)
+	{
+		ScanKey		key = so->keyData + startikey;
+		BTArrayKeyInfo *array;
+		Datum		firstdatum,
+					lastdatum;
+		bool		firstnull,
+					lastnull;
+		int32		result;
+
+		/*
+		 * Determine if it's safe to set pstate.startikey to an offset to a
+		 * key that comes after this key, by examining this key
+		 */
+		if (unlikely(!(key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))))
+		{
+			/*
+			 * We only expect to get to a key that's not marked required when
+			 * preprocessing didn't generate a skip array for some prior
+			 * attribute due to its input opclass lacking a usable = operator
+			 * (preprocessing handles RowCompare keys similarly, but we know
+			 * that that can't be the explanation for this, since we'd have
+			 * seen the RowCompare and set pstate.startikey to its offset
+			 * already).
+			 *
+			 * This is a rare edge-case, so handle it by giving up completely.
+			 */
+			return;
+		}
+		if (key->sk_flags & SK_ROW_HEADER)
+		{
+			/*
+			 * Can't let pstate.startikey get set to an ikey beyond a
+			 * RowCompare inequality
+			 */
+			break;				/* unsafe */
+		}
+		if (key->sk_strategy != BTEqualStrategyNumber)
+		{
+			/*
+			 * Scalar inequality key.
+			 *
+			 * It's definitely safe for _bt_checkkeys to avoid assessing this
+			 * inequality when the page's first and last non-pivot tuples both
+			 * satisfy the inequality (since the same must also be true of all
+			 * the tuples in between these two).
+			 *
+			 * Unlike the "=" case, it doesn't matter if this attribute has
+			 * more than one distinct value (though it _is_ necessary for any
+			 * and all _prior_ attributes to contain no more than one distinct
+			 * value amongst all of the tuples from pstate.page).
+			 */
+			if (key->sk_attno > firstchangingattnum)	/* >, not >= */
+				break;			/* unsafe, preceding attr has multiple
+								 * distinct values */
+
+			firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &firstnull);
+			lastdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &lastnull);
+
+			if (key->sk_flags & SK_ISNULL)
+			{
+				/* IS NOT NULL key */
+				Assert(key->sk_flags & SK_SEARCHNOTNULL);
+
+				if (firstnull || lastnull)
+					break;		/* unsafe */
+
+				/* Safe, IS NOT NULL key satisfied by every tuple */
+				continue;
+			}
+
+			/* Test firsttup */
+			if (firstnull ||
+				!DatumGetBool(FunctionCall2Coll(&key->sk_func,
+												key->sk_collation, firstdatum,
+												key->sk_argument)))
+				break;			/* unsafe */
+
+			/* Test lasttup */
+			if (lastnull ||
+				!DatumGetBool(FunctionCall2Coll(&key->sk_func,
+												key->sk_collation, lastdatum,
+												key->sk_argument)))
+				break;			/* unsafe */
+
+			/* Safe, scalar inequality satisfied by every tuple */
+			continue;
+		}
+
+		/* Some = key (could be a a scalar = key, could be an array = key) */
+		Assert(key->sk_strategy == BTEqualStrategyNumber);
+
+		if (!(key->sk_flags & SK_SEARCHARRAY))
+		{
+			/*
+			 * Scalar = key (posibly an IS NULL key).
+			 *
+			 * It is unsafe to set pstate.startikey to an ikey beyond this
+			 * key, unless the = key is satisfied by every possible tuple on
+			 * the page (possible only when attribute has just one distinct
+			 * value among all tuples on the page).
+			 */
+			if (key->sk_attno >= firstchangingattnum)
+				break;			/* unsafe, multiple distinct attr values */
+
+			firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+									   &firstnull);
+			if (key->sk_flags & SK_ISNULL)
+			{
+				/* IS NULL key */
+				Assert(key->sk_flags & SK_SEARCHNULL);
+
+				if (!firstnull)
+					break;		/* unsafe */
+
+				/* Safe, IS NULL key satisfied by every tuple */
+				continue;
+			}
+			if (firstnull ||
+				!DatumGetBool(FunctionCall2Coll(&key->sk_func,
+												key->sk_collation, firstdatum,
+												key->sk_argument)))
+				break;			/* unsafe */
+
+			/* Safe, scalar = key satisfied by every tuple */
+			continue;
+		}
+
+		/* = array key (could be a SAOP array, could be a skip array) */
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == startikey);
+		if (array->num_elems != -1)
+		{
+			/*
+			 * SAOP array = key.
+			 *
+			 * Handle this like we handle scalar = keys (though binary search
+			 * for a matching element, to avoid relying on key's sk_argument).
+			 */
+			if (key->sk_attno >= firstchangingattnum)
+				break;			/* unsafe, multiple distinct attr values */
+
+			if (startikey < so->numberOfKeys - 1 &&
+				so->keyData[startikey + 1].sk_strategy == BTEqualStrategyNumber &&
+				!so->skipScan)
+			{
+				/*
+				 * SAOP array = key isn't the least significant = key during a
+				 * scan without any skip arrays.
+				 *
+				 * We can safely set startikey to an offset beyond this key
+				 * (in the likely event that the = key is actually satisfied).
+				 * But it would only be safe if we set forcenonrequired=true.
+				 *
+				 * Back out now, so that the scan can use the "look-ahead"
+				 * optimization (and _bt_start_array_keys' cur_elem_trig
+				 * optimization) instead.
+				 */
+				break;
+			}
+
+			firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+									   &firstnull);
+			_bt_binsrch_array_skey(&so->orderProcs[startikey],
+								   false, NoMovementScanDirection,
+								   firstdatum, firstnull, array, key,
+								   &result);
+			if (result != 0)
+				break;			/* unsafe */
+
+			/* Safe, SAOP = key satisfied by every tuple */
+			start_past_saop_eq = true;
+			continue;
+		}
+
+		/*
+		 * Skip array = key.
+		 *
+		 * Handle this like we handle scalar inequality keys (but avoid using
+		 * key's sk_argument/advancing array, as in the SAOP array case).
+		 */
+		if (array->null_elem)
+		{
+			/*
+			 * Safe, non-range skip array "satisfied" by every tuple on page
+			 * (safe even when "key->sk_attno <= firstchangingattnum")
+			 */
+			continue;
+		}
+		else if (key->sk_attno > firstchangingattnum)	/* >, not >= */
+		{
+			break;				/* unsafe, preceding attr has multiple
+								 * distinct values */
+		}
+
+		firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &firstnull);
+		lastdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &lastnull);
+
+		/* Test firsttup */
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   firstdatum, firstnull, array, key,
+								   &result);
+		if (result != 0)
+			break;				/* unsafe */
+
+		/* Test lasttup */
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   lastdatum, lastnull, array, key,
+								   &result);
+		if (result != 0)
+			break;				/* unsafe */
+
+		/* Safe, range skip array satisfied by every tuple */
+	}
+
+	/*
+	 * Use of forcenonrequired is typically undesirable, since it'll force
+	 * _bt_readpage caller to read every tuple on the page -- even though, in
+	 * general, it might well be possible to end the scan on an earlier tuple.
+	 * However, caller must use forcenonrequired when start_past_saop_eq=true,
+	 * since the usual required array behavior might fail to roll over to the
+	 * SAOP array.
+	 *
+	 * We always use forcenonrequired during scans with skip arrays (except on
+	 * the first page of each primitive index scan), though.  While it'd be
+	 * possible to extend the start_past_saop_eq behavior to skip arrays, we
+	 * prefer to always use forcenonrequired -- even when "startikey == 0".
+	 * That way _bt_advance_array_keys's low-order key precheck optimization
+	 * can always be used (unless on the first page of the scan).  In general
+	 * it's worth checking more tuples if that allows us to do significantly
+	 * less skip array maintenance.
+	 */
+	pstate->forcenonrequired = (start_past_saop_eq || so->skipScan);
+	pstate->startikey = startikey;
+
+	if (!pstate->forcenonrequired)
+		return;
+
+	Assert(so->numArrayKeys);
+
+	/*
+	 * Set the element for arrays whose ikey is >= startikey to the lowest
+	 * element value (set it to the highest value when scanning backwards).
+	 * But don't modify the current array element for any earlier arrays.
+	 *
+	 * This allows skip arrays that will never be satisfied by any tuple on
+	 * the page to avoid extra sk_argument comparisons.  _bt_check_compare
+	 * won't use the key's sk_argument when the key is marked MINVAL/MAXVAL.
+	 * Moreover, it's unlikely that MINVAL/MAXVAL will be set for one of these
+	 * arrays unless _bt_advance_array_keys can find an exact match element
+	 * (nonrequired arrays won't "advance" unless an exact match element is
+	 * found, though even that is unlikely due to _bt_advance_array_keys's
+	 * precheck forcing an early "return false", before examining any array).
+	 */
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		key = &so->keyData[array->scan_key];
+
+		if (array->scan_key < startikey)
+			continue;
+
+		_bt_array_set_low_or_high(rel, key, array,
+								  ScanDirectionIsForward(dir));
+	}
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
@@ -2431,23 +2782,33 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  * by the current array key, or if they're truly unsatisfied (that is, if
  * they're unsatisfied by every possible array key).
  *
- * Though we advance non-required array keys on our own, that shouldn't have
- * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
- *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass forcenonrequired=true to instruct us to treat all keys as nonrequired.
+ * This is used to make it safe to temporarily stop properly maintaining the
+ * scan's required arrays.  _bt_checkkeys caller (_bt_readpage, actually)
+ * determines a prefix of keys that must satisfy every possible corresponding
+ * index attribute value from its page, which is passed to us via *ikey arg
+ * (this is the first key that might be unsatisfied by tuples on the page).
+ * Obviously, we won't maintain any array keys from before *ikey, so it's
+ * quite possible for such arrays to "fall behind" the index's keyspace.
+ * Caller will need to "catch up" by passing forcenonrequired=true (alongside
+ * an *ikey=0) once the page's finaltup is reached.
+ *
+ * Note: it's safe to pass an *ikey > 0 with forcenonrequired=false, but only
+ * when caller determines that it won't affect array maintenance.
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool forcenonrequired,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
+	Assert(!forcenonrequired || advancenonrequired);
+
 	*continuescan = true;		/* default assumption */
 
 	for (; *ikey < so->numberOfKeys; (*ikey)++)
@@ -2460,36 +2821,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when we're forced to treat all scan keys as nonrequired)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (forcenonrequired)
+		{
+			/* treating scan's keys as non-required */
+		}
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
 			requiredOppositeDirOnly = true;
 
-		/*
-		 * If the caller told us the *continuescan flag is known to be true
-		 * for the last item on the page, then we know the keys required for
-		 * the current direction scan should be matched.  Otherwise, the
-		 * *continuescan flag would be set for the current item and
-		 * subsequently the last item on the page accordingly.
-		 *
-		 * If the key is required for the opposite direction scan, we can skip
-		 * the check if the caller tells us there was already at least one
-		 * matching item on the page. Also, we require the *continuescan flag
-		 * to be true for the last item on the page to know there are no
-		 * NULLs.
-		 *
-		 * Both cases above work except for the row keys, where NULLs could be
-		 * found in the middle of matching values.
-		 */
-		if (prechecked &&
-			(requiredSameDir || (requiredOppositeDirOnly && firstmatch)) &&
-			!(key->sk_flags & SK_ROW_HEADER))
-			continue;
-
 		if (key->sk_attno > tupnatts)
 		{
 			/*
@@ -2511,6 +2856,19 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		{
 			Assert(key->sk_flags & SK_SEARCHARRAY);
 			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir || forcenonrequired);
+
+			/*
+			 * Cannot fall back on _bt_tuple_before_array_skeys when we're
+			 * treating the scan's keys as nonrequired, though.  Just handle
+			 * this like any other non-required equality-type array key.
+			 */
+			if (forcenonrequired)
+			{
+				Assert(!(key->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+				return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
+											  tupdesc, *ikey, false);
+			}
 
 			*continuescan = false;
 			return false;
@@ -2520,7 +2878,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
-									 continuescan))
+									 forcenonrequired, continuescan))
 				continue;
 			return false;
 		}
@@ -2553,9 +2911,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			 */
 			if (requiredSameDir)
 				*continuescan = false;
+			else if (unlikely(key->sk_flags & SK_BT_SKIP))
+			{
+				/*
+				 * If we're treating scan keys as nonrequired, and encounter a
+				 * skip array scan key whose current element is NULL, then it
+				 * must be a non-range skip array.  It must be satisfied, so
+				 * there's no need to call _bt_advance_array_keys to check.
+				 */
+				Assert(forcenonrequired && *ikey > 0);
+				continue;
+			}
 
 			/*
-			 * In any case, this indextuple doesn't match the qual.
+			 * This indextuple doesn't match the qual.
 			 */
 			return false;
 		}
@@ -2576,7 +2945,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -2594,7 +2963,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -2605,15 +2974,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			return false;
 		}
 
-		/*
-		 * Apply the key-checking function, though only if we must.
-		 *
-		 * When a key is required in the opposite-of-scan direction _only_,
-		 * then it must already be satisfied if firstmatch=true indicates that
-		 * an earlier tuple from this same page satisfied it earlier on.
-		 */
-		if (!(requiredOppositeDirOnly && firstmatch) &&
-			!DatumGetBool(FunctionCall2Coll(&key->sk_func, key->sk_collation,
+		if (!DatumGetBool(FunctionCall2Coll(&key->sk_func, key->sk_collation,
 											datum, key->sk_argument)))
 		{
 			/*
@@ -2663,7 +3024,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
  */
 static bool
 _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
-					 TupleDesc tupdesc, ScanDirection dir, bool *continuescan)
+					 TupleDesc tupdesc, ScanDirection dir,
+					 bool forcenonrequired, bool *continuescan)
 {
 	ScanKey		subkey = (ScanKey) DatumGetPointer(skey->sk_argument);
 	int32		cmpresult = 0;
@@ -2703,7 +3065,11 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 
 		if (isNull)
 		{
-			if (subkey->sk_flags & SK_BT_NULLS_FIRST)
+			if (forcenonrequired)
+			{
+				/* treating scan's keys as non-required */
+			}
+			else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -2757,8 +3123,12 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			 */
 			Assert(subkey != (ScanKey) DatumGetPointer(skey->sk_argument));
 			subkey--;
-			if ((subkey->sk_flags & SK_BT_REQFWD) &&
-				ScanDirectionIsForward(dir))
+			if (forcenonrequired)
+			{
+				/* treating scan's keys as non-required */
+			}
+			else if ((subkey->sk_flags & SK_BT_REQFWD) &&
+					 ScanDirectionIsForward(dir))
 				*continuescan = false;
 			else if ((subkey->sk_flags & SK_BT_REQBKWD) &&
 					 ScanDirectionIsBackward(dir))
@@ -2810,7 +3180,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			break;
 	}
 
-	if (!result)
+	if (!result && !forcenonrequired)
 	{
 		/*
 		 * Tuple fails this qual.  If it's a required qual for the current
@@ -2854,6 +3224,8 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	Assert(!pstate->forcenonrequired);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
-- 
2.49.0

v31-0003-Apply-low-order-skip-key-in-_bt_first-more-often.patchapplication/octet-stream; name=v31-0003-Apply-low-order-skip-key-in-_bt_first-more-often.patchDownload
From d6eeb604de046d1ef4a9a1c9bc287a6233b1378d Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 13 Jan 2025 16:08:32 -0500
Subject: [PATCH v31 3/4] Apply low-order skip key in _bt_first more often.

Convert low_compare and high_compare nbtree skip array inequalities
(with opclasses that offer skip support) such that _bt_first is
consistently able to use later keys when descending the tree within
_bt_first.

For example, an index qual "WHERE a > 5 AND b = 2" is now converted to
"WHERE a >= 6 AND b = 2" by a new preprocessing step that takes place
after a final low_compare and/or high_compare are chosen by all earlier
preprocessing steps.  That way the scan's initial call to _bt_first will
use "WHERE a >= 6 AND b = 2" to find the initial leaf level position,
rather than merely using "WHERE a > 5" -- "b = 2" can always be applied.
This has a decent chance of making the scan avoid an extra _bt_first
call that would otherwise be needed just to determine the lowest-sorting
"a" value in the index (the lowest that still satisfies "WHERE a > 5").

The transformation process can only lower the total number of index
pages read when the use of a more restrictive set of initial positioning
keys in _bt_first actually allows the scan to land on some later leaf
page directly, relative to the unoptimized case (or on an earlier leaf
page directly, when scanning backwards).  The savings can be far greater
when affected skip arrays come after some higher-order array.  For
example, a qual "WHERE x IN (1, 2, 3) AND y > 5 AND z = 2" can now save
as many as 3 _bt_first calls as a result of these transformations (there
can be as many as 1 _bt_first call saved per "x" array element).

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Discussion: https://postgr.es/m/CAH2-Wz=FJ78K3WsF3iWNxWnUCY9f=Jdg3QPxaXE=uYUbmuRz5Q@mail.gmail.com
---
 src/backend/access/nbtree/nbtpreprocesskeys.c | 180 ++++++++++++++++++
 src/test/regress/expected/create_index.out    |  21 ++
 src/test/regress/sql/create_index.sql         |  10 +
 3 files changed, 211 insertions(+)

diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 5689e24b7..7b2b0c44e 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -50,6 +50,12 @@ static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
 								 BTArrayKeyInfo *array, bool *qual_ok);
 static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
 								 BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+									   BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
@@ -1290,6 +1296,171 @@ _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
 	return true;
 }
 
+/*
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts their
+ * operator strategies, while changing the operator as appropriate).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value MINVAL, using a transformed >= key
+ * instead of using the original > key makes it safe to include lower-order
+ * scan keys in the insertion scan key (there must be lower-order scan keys
+ * after the skip array).  We will avoid an extra _bt_first to find the first
+ * value in the index > sk_argument -- at least when the first real matching
+ * value in the index happens to be an exact match for the sk_argument value
+ * that we produced here by incrementing the original input key's sk_argument.
+ * (Backwards scans derive the same benefit when they encounter the sentinel
+ * value MAXVAL, by converting the high_compare key from < to <=.)
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01'.
+ *
+ * Note: We can safely modify array->low_compare/array->high_compare in place
+ * because they just point to copies of our scan->keyData[] input scan keys
+ * (namely the copies returned by _bt_preprocess_array_keys to be used as
+ * input into the standard preprocessing steps in _bt_preprocess_keys).
+ * Everything will be reset if there's a rescan.
+ */
+static void
+_bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+						   BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	/*
+	 * Called last among all preprocessing steps, when the skip array's final
+	 * low_compare and high_compare have both been chosen
+	 */
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1 && !array->null_elem && array->sksup);
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skiparray_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skiparray_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1825,6 +1996,15 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && array->sksup &&
+						!array->null_elem)
+						_bt_skiparray_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 15dde752f..fa4517e2d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2589,6 +2589,27 @@ ORDER BY thousand;
         1 |     1001
 (1 row)
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+                                            QUERY PLAN                                            
+--------------------------------------------------------------------------------------------------
+ Limit
+   ->  Index Only Scan using tenk1_thous_tenthous on tenk1
+         Index Cond: ((thousand > '-1'::integer) AND (tenthous = ANY ('{1001,3000}'::integer[])))
+(3 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+        1 |     1001
+(2 rows)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index 40ba3d65b..e8c16959a 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -993,6 +993,16 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
 ORDER BY thousand;
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
-- 
2.49.0

v31-0004-DEBUG-Add-skip-scan-disable-GUCs.patchapplication/octet-stream; name=v31-0004-DEBUG-Add-skip-scan-disable-GUCs.patchDownload
From bff51d480bac2a36f6e2edfbfe772261a7c290b1 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 18 Jan 2025 10:54:44 -0500
Subject: [PATCH v31 4/4] DEBUG: Add skip scan disable GUCs.

---
 src/include/access/nbtree.h                   |  5 +++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 37 +++++++++++++++++++
 src/backend/access/nbtree/nbtutils.c          |  3 ++
 src/backend/utils/misc/guc_tables.c           | 34 +++++++++++++++++
 4 files changed, 79 insertions(+)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index c8708f2fd..dea7f87c0 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1177,6 +1177,11 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+extern PGDLLIMPORT bool skipscan_iprefix_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 7b2b0c44e..58f639ed2 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -21,6 +21,33 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
+/*
+ * skipscan_iprefix_enabled can be used to disable optimizations used when the
+ * maintenance overhead of skip arrays stops paying for itself
+ */
+bool		skipscan_iprefix_enabled = true;
+
 typedef struct BTScanKeyPreproc
 {
 	ScanKey		inkey;
@@ -1643,6 +1670,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
 			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
 
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].sksup = NULL;
+
 			/*
 			 * We'll need a 3-way ORDER proc.  Set that up now.
 			 */
@@ -2146,6 +2177,12 @@ _bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
 		if (attno_has_rowcompare)
 			break;
 
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
 		/*
 		 * Now consider next attno_inkey (or keep going if this is an
 		 * additional scan key against the same attribute)
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 2dddaba7a..3d26bea92 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -2475,6 +2475,9 @@ _bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate)
 	if (so->numberOfKeys == 0)
 		return;
 
+	if (!skipscan_iprefix_enabled)
+		return;
+
 	/* minoff is an offset to the lowest non-pivot tuple on the page */
 	iid = PageGetItemId(pstate->page, pstate->minoff);
 	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 989825d3a..4a37044cd 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1788,6 +1789,28 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
+	/* XXX Remove before commit */
+	{
+		{"skipscan_iprefix_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_iprefix_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3734,6 +3757,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
-- 
2.49.0

v31-0001-Add-nbtree-skip-scan-optimizations.patchapplication/octet-stream; name=v31-0001-Add-nbtree-skip-scan-optimizations.patchDownload
From a848615dd81482cc047c7ffcb13a15b03cc86153 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v31 1/4] Add nbtree skip scan optimizations.

Teach nbtree composite index scans to opportunistically skip over
irrelevant sections of composite indexes given a query with an omitted
prefix column.  When nbtree is passed input scan keys derived from a
query predicate "WHERE b = 5", new nbtree preprocessing steps now output
"WHERE a = ANY(<every possible 'a' value>) AND b = 5" scan keys.  That
is, preprocessing generates a "skip array" (along with an associated
scan key) for the omitted column "a", which makes it safe to mark the
scan key on "b" as required to continue the scan.  This is far more
efficient than a traditional full index scan whenever it allows the scan
to skip over many irrelevant leaf pages, by iteratively repositioning
itself using the keys on "a" and "b" together.

A skip array has "elements" that are generated procedurally and on
demand, but otherwise works just like a regular ScalarArrayOp array.
Preprocessing can freely add a skip array before or after any input
ScalarArrayOp arrays.  Index scans with a skip array decide when and
where to reposition the scan using the same approach as any other scan
with array keys.  This design builds on the design for array advancement
and primitive scan scheduling added to Postgres 17 by commit 5bf748b8.

The core B-Tree operator classes on most discrete types generate their
array elements with the help of their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the next
possible indexable value frequently turns out to be the next value
stored in the index.  Opclasses that lack a skip support routine fall
back on having nbtree "increment" (or "decrement") a skip array's
current element by setting the NEXT (or PRIOR) scan key flag, without
directly changing the scan key's sk_argument.  These sentinel values
behave just like any other value from an array -- though they can never
locate equal index tuples (they can only locate the next group of index
tuples containing the next set of non-sentinel values that the scan's
arrays need to advance to).

Inequality scan keys can affect how skip arrays generate their values.
Their range is constrained by the inequalities.  For example, a skip
array on "a" will only use element values 1 and 2 given a qual such as
"WHERE a BETWEEN 1 AND 2 AND b = 66".  A scan using such a skip array
has almost identical performance characteristics to one with the qual
"WHERE a = ANY('{1, 2}') AND b = 66".  The scan will be much faster when
it can be executed as two selective primitive index scans instead of a
single very large index scan that reads many irrelevant leaf pages.
However, the array transformation process won't always lead to improved
performance at runtime.  Much depends on physical index characteristics.

B-Tree preprocessing is optimistic about skipping working out: it
applies static, generic rules when determining where to generate skip
arrays, which assumes that the runtime overhead of maintaining skip
arrays will pay for itself -- or lead to only a modest performance loss.
As things stand, these assumptions are much too optimistic: skip array
maintenance will lead to unacceptable regressions with unsympathetic
queries (queries whose scan has little to no chance of avoid reading
irrelevant leaf pages).  An upcoming commit will address the problems in
this area by enhancing _bt_readpage's approach to saving cycles on scan
key evaluation, making it work in a way that directly considers the
needs of = array keys (particularly = skip array keys).

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |   3 +-
 src/include/access/nbtree.h                   |  34 +-
 src/include/catalog/pg_amproc.dat             |  22 +
 src/include/catalog/pg_proc.dat               |  27 +
 src/include/utils/skipsupport.h               |  98 +++
 src/backend/access/index/indexam.c            |   3 +-
 src/backend/access/nbtree/nbtcompare.c        | 273 +++++++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 598 ++++++++++++--
 src/backend/access/nbtree/nbtree.c            | 195 ++++-
 src/backend/access/nbtree/nbtsearch.c         | 130 ++-
 src/backend/access/nbtree/nbtutils.c          | 755 ++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |   4 +
 src/backend/commands/opclasscmds.c            |  25 +
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/date.c                  |  46 ++
 src/backend/utils/adt/meson.build             |   1 +
 src/backend/utils/adt/selfuncs.c              | 490 +++++++++---
 src/backend/utils/adt/skipsupport.c           |  61 ++
 src/backend/utils/adt/timestamp.c             |  48 ++
 src/backend/utils/adt/uuid.c                  |  70 ++
 doc/src/sgml/btree.sgml                       |  34 +-
 doc/src/sgml/indexam.sgml                     |   3 +-
 doc/src/sgml/indices.sgml                     |  49 +-
 doc/src/sgml/monitoring.sgml                  |   4 +-
 doc/src/sgml/perform.sgml                     |  31 +
 doc/src/sgml/xindex.sgml                      |  16 +-
 src/test/regress/expected/alter_generic.out   |  10 +-
 src/test/regress/expected/btree_index.out     |  41 +
 src/test/regress/expected/create_index.out    | 183 ++++-
 src/test/regress/expected/psql.out            |   3 +-
 src/test/regress/sql/alter_generic.sql        |   5 +-
 src/test/regress/sql/btree_index.sql          |  21 +
 src/test/regress/sql/create_index.sql         |  63 +-
 src/tools/pgindent/typedefs.list              |   3 +
 34 files changed, 2969 insertions(+), 381 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c4a073773..52916bab7 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -214,7 +214,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index faabcb78e..b86bf7bf3 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -707,6 +708,10 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
  *	(BTOPTIONS_PROC).  These procedures define a set of user-visible
  *	parameters that can be used to control operator class behavior.  None of
  *	the built-in B-Tree operator classes currently register an "options" proc.
+ *
+ *	To facilitate more efficient B-Tree skip scans, an operator class may
+ *	choose to offer a sixth amproc procedure (BTSKIPSUPPORT_PROC).  For full
+ *	details, see src/include/utils/skipsupport.h.
  */
 
 #define BTORDER_PROC		1
@@ -714,7 +719,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1027,10 +1033,21 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
-	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for ScalarArrayOpExpr arrays */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+	int			cur_elem;		/* index of current element in elem_values */
+
+	/* fields for skip arrays, which generate element datums procedurally */
+	int16		attlen;			/* attr's length, in bytes */
+	bool		attbyval;		/* attr's FormData_pg_attribute.attbyval */
+	bool		null_elem;		/* NULL is lowest/highest element? */
+	SkipSupport sksup;			/* skip support (NULL if opclass lacks it) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1119,6 +1136,15 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on column without input = */
+
+/* SK_BT_SKIP-only flags (set and unset by array advancement) */
+#define SK_BT_MINVAL	0x00080000	/* invalid sk_argument, use low_compare */
+#define SK_BT_MAXVAL	0x00100000	/* invalid sk_argument, use high_compare */
+#define SK_BT_NEXT		0x00200000	/* positions the scan > sk_argument */
+#define SK_BT_PRIOR		0x00400000	/* positions the scan < sk_argument */
+
+/* Remaps pg_index flag bits to uppermost SK_BT_* byte */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1165,7 +1191,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 19100482b..37000639f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3f7b82e02..870d4844c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2288,6 +2303,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4478,6 +4496,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6357,6 +6378,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9405,6 +9429,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..bc51847cf
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining groups of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * It usually isn't feasible to implement skip support for an opclass whose
+ * input type is continuous.  The B-Tree code falls back on next-key sentinel
+ * values for any opclass that doesn't provide its own skip support function.
+ * This isn't really an implementation restriction; there is no benefit to
+ * providing skip support for an opclass where guessing that the next indexed
+ * value is the next possible indexable value never (or hardly ever) works out.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order)
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 * Operator class decrement/increment functions will never be called with
+	 * a NULL "existing" argument, either.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern SkipSupport PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+												 bool reverse);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 55ec4c103..219df1971 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -489,7 +489,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	if (parallel_aware &&
 		indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 291cb8fc1..4da5a3c1d 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 38a87af1c..09cd9d751 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -45,8 +45,15 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
+static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
+								 ScanKey skey, FmgrInfo *orderproc,
+								 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
+								 BTArrayKeyInfo *array, bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
+							   int *numSkipArrayKeys);
 static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
 									  Oid elemtype, StrategyNumber strat,
 									  Datum *elems, int nelems);
@@ -89,6 +96,8 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -101,10 +110,16 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never generated skip array scan keys, it would be possible for "gaps"
+ * to appear that make it unsafe to mark any subsequent input scan keys
+ * (copied from scan->keyData[]) as required to continue the scan.  Prior to
+ * Postgres 18, a qual like "WHERE y = 4" always required a full index scan.
+ * It'll now become "WHERE x = ANY('{every possible x value}') and y = 4" on
+ * output, due to the addition of a skip array on "x".  This has the potential
+ * to be much more efficient than a full index scan (though it'll behave like
+ * a full index scan when there are many distinct "x" values).
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -141,7 +156,12 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Note, however, that we are not able to replace a row
+ * comparison with a skip array due to the design of _bt_check_rowcompare.
+ * Row comparisons are treated like a comparison of the first/leftmost column
+ * from the row argument, but actually compare multiple columns internally.
+ * That isn't compatible with skip arrays, which work by making a value into
+ * the current element, anchoring later scan keys via the "=" constraint.
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -200,6 +220,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProcs[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -229,6 +257,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			Assert(so->keyData[0].sk_flags & SK_SEARCHARRAY);
 			Assert(so->keyData[0].sk_strategy != BTEqualStrategyNumber ||
 				   (so->arrayKeys[0].scan_key == 0 &&
+					!(so->keyData[0].sk_flags & SK_BT_SKIP) &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
 
@@ -288,7 +317,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -345,7 +375,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -393,6 +422,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, _bt_preprocess_array_keys has already added "=" keys
+			 * sufficient to form an unbroken series of "=" constraints on all
+			 * attrs prior to the attr from the final scan->keyData[] key.
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -481,6 +515,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -489,6 +524,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -508,8 +544,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -803,6 +837,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -812,6 +849,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -885,6 +938,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -978,24 +1032,55 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
  * Compare an array scan key to a scalar scan key, eliminating contradictory
  * array elements such that the scalar scan key becomes redundant.
  *
+ * If the opfamily is incomplete we may not be able to determine which
+ * elements are contradictory.  When we return true we'll have validly set
+ * *qual_ok, guaranteeing that at least the scalar scan key can be considered
+ * redundant.  We return false if the comparison could not be made (caller
+ * must keep both scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar scan keys.
+ */
+static bool
+_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
+							   bool *qual_ok)
+{
+	Assert(arraysk->sk_attno == skey->sk_attno);
+	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
+		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
+	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
+		   skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Just call the appropriate helper function based on whether it's a SAOP
+	 * array or a skip array.  Both helpers will set *qual_ok in passing.
+	 */
+	if (array->num_elems != -1)
+		return _bt_saoparray_shrink(scan, arraysk, skey, orderproc, array,
+									qual_ok);
+	else
+		return _bt_skiparray_shrink(scan, skey, array, qual_ok);
+}
+
+/*
+ * Preprocessing of SAOP (non-skip) array scan key, used to determine which
+ * array elements are eliminated as contradictory by a non-array scalar key.
+ * _bt_compare_array_scankey_args helper function.
+ *
  * Array elements can be eliminated as contradictory when excluded by some
  * other operator on the same attribute.  For example, with an index scan qual
  * "WHERE a IN (1, 2, 3) AND a < 2", all array elements except the value "1"
  * are eliminated, and the < scan key is eliminated as redundant.  Cases where
  * every array element is eliminated by a redundant scalar scan key have an
  * unsatisfiable qual, which we handle by setting *qual_ok=false for caller.
- *
- * If the opfamily doesn't supply a complete set of cross-type ORDER procs we
- * may not be able to determine which elements are contradictory.  If we have
- * the required ORDER proc then we return true (and validly set *qual_ok),
- * guaranteeing that at least the scalar scan key can be considered redundant.
- * We return false if the comparison could not be made (caller must keep both
- * scan keys when this happens).
  */
 static bool
-_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
-							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
-							   bool *qual_ok)
+_bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+					 FmgrInfo *orderproc, BTArrayKeyInfo *array, bool *qual_ok)
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
@@ -1006,14 +1091,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
-	Assert(arraysk->sk_attno == skey->sk_attno);
 	Assert(array->num_elems > 0);
-	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
-		   arraysk->sk_strategy == BTEqualStrategyNumber);
-	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
-		   skey->sk_strategy != BTEqualStrategyNumber);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
 
 	/*
 	 * _bt_binsrch_array_skey searches an array for the entry best matching a
@@ -1112,6 +1191,105 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	return true;
 }
 
+/*
+ * Preprocessing of skip (non-SAOP) array scan key, used to determine
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function.
+ *
+ * Unlike _bt_saoparray_shrink, we don't modify caller's array in-place.  Skip
+ * arrays work by procedurally generating their elements as needed, so we just
+ * store the inequality as the skip array's low_compare or high_compare.  The
+ * array's elements will be generated from the range of values that satisfies
+ * both low_compare and high_compare.
+ */
+static bool
+_bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
+					 bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Array's index attribute will be constrained by a strict operator/key.
+	 * Array must not "contain a NULL element" (i.e. the scan must not apply
+	 * "IS NULL" qual when it reaches the end of the index that stores NULLs).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Consider if we should treat caller's scalar scan key as the skip
+	 * array's high_compare or low_compare.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way the scan can always safely assume that
+	 * it's okay to use the input-opclass-only-type proc from so->orderProcs[]
+	 * (they can be cross-type with SAOP arrays, but never with skip arrays).
+	 *
+	 * This approach is enabled by MINVAL/MAXVAL sentinel key markings, which
+	 * can be thought of as representing either the lowest or highest matching
+	 * array element (excluding the NULL element, where applicable, though as
+	 * just discussed it isn't applicable to this range skip array anyway).
+	 * Array keys marked MINVAL/MAXVAL never have a valid datum in their
+	 * sk_argument field.  The scan directly applies the array's low_compare
+	 * key when it encounters MINVAL in the array key proper (just as it
+	 * applies high_compare when it sees MAXVAL set in the array key proper).
+	 * The scan must never use the array's so->orderProcs[] proc against
+	 * low_compare's/high_compare's sk_argument, either (so->orderProcs[] is
+	 * only intended to be used with rhs datums from the array proper/index).
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* replace existing high_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing high_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's high_compare */
+			array->high_compare = skey;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* replace existing low_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing low_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's low_compare */
+			array->low_compare = skey;
+			break;
+		case BTEqualStrategyNumber:
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1137,6 +1315,12 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -1152,41 +1336,35 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	Oid			skip_eq_ops[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
-	ScanKey		cur;
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys we'll need to generate below
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -1201,18 +1379,20 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
+		ScanKey		inkey = scan->keyData + input_ikey,
+					cur;
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
 		Oid			elemtype;
@@ -1225,21 +1405,113 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		Datum	   *elem_values;
 		bool	   *elem_nulls;
 		int			num_nonnulls;
-		int			j;
+
+		/* set up next output scan key */
+		cur = &arrayKeyData[numArrayKeyData];
+
+		/* Backfill skip arrays for attrs < or <= input key's attr? */
+		while (numSkipArrayKeys && attno_skip <= inkey->sk_attno)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skip_eq_ops[attno_skip - 1];
+			CompactAttribute *attr;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/*
+				 * Attribute already has an = input key, so don't output a
+				 * skip array for attno_skip.  Just copy attribute's = input
+				 * key into arrayKeyData[] once outside this inner loop.
+				 *
+				 * Note: When we get here there must be a later attribute that
+				 * lacks an equality input key, and still needs a skip array
+				 * (if there wasn't then numSkipArrayKeys would be 0 by now).
+				 */
+				Assert(attno_skip == inkey->sk_attno);
+				/* inkey can't be last input key to be marked required: */
+				Assert(input_ikey < scan->numberOfKeys - 1);
+#if 0
+				/* Could be a redundant input scan key, so can't do this: */
+				Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+					   (inkey->sk_flags & SK_SEARCHNULL));
+#endif
+
+				attno_skip++;
+				break;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize generic BTArrayKeyInfo fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+
+			/* Initialize skip array specific BTArrayKeyInfo fields */
+			attr = TupleDescCompactAttr(RelationGetDescr(rel), attno_skip - 1);
+			reverse = (indoption[attno_skip - 1] & INDOPTION_DESC) != 0;
+			so->arrayKeys[numArrayKeys].attlen = attr->attlen;
+			so->arrayKeys[numArrayKeys].attbyval = attr->attbyval;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup =
+				PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse);
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * We'll need a 3-way ORDER proc.  Set that up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			numArrayKeys++;
+			numArrayKeyData++;	/* keep this scan key/array */
+
+			/* set up next output scan key */
+			cur = &arrayKeyData[numArrayKeyData];
+
+			/* remember having output this skip array and scan key */
+			numSkipArrayKeys--;
+			attno_skip++;
+		}
 
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
-		*cur = scan->keyData[input_ikey];
+		*cur = *inkey;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -1257,7 +1529,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * all btree operators are strict.
 		 */
 		num_nonnulls = 0;
-		for (j = 0; j < num_elems; j++)
+		for (int j = 0; j < num_elems; j++)
 		{
 			if (!elem_nulls[j])
 				elem_values[num_nonnulls++] = elem_values[j];
@@ -1295,7 +1567,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -1306,7 +1578,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -1323,7 +1595,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -1392,23 +1664,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			origelemtype = elemtype;
 		}
 
-		/*
-		 * And set up the BTArrayKeyInfo data.
-		 *
-		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
-		 * scan_key field later on, after so->keyData[] has been finalized.
-		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		/* Initialize generic BTArrayKeyInfo fields */
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
+
+		/* Initialize SAOP array specific BTArrayKeyInfo fields */
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].cur_elem = -1;	/* i.e. invalid */
+
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
+	Assert(numSkipArrayKeys == 0);
+
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -1514,7 +1787,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -1575,6 +1849,188 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit every so->arrayKeys[] entry.
+ *
+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller must add this many
+ * skip arrays to each of the most significant attributes lacking any keys
+ * that use the = strategy (IS NULL keys count as = keys here).  The specific
+ * attributes that need skip arrays are indicated by initializing caller's
+ * skip_eq_ops[] 0-based attribute offset to a valid = op strategy Oid.  We'll
+ * only ever set skip_eq_ops[] entries to InvalidOid for attributes that
+ * already have an equality key in scan->keyData[] input keys -- and only when
+ * there's some later "attribute gap" for us to "fill-in" with a skip array.
+ *
+ * We're optimistic about skipping working out: we always add exactly the skip
+ * arrays needed to maximize the number of input scan keys that can ultimately
+ * be marked as required to continue the scan (but no more).  For a composite
+ * index on (a, b, c, d), we'll instruct caller to add skip arrays as follows:
+ *
+ * Input keys						Output keys (after all preprocessing)
+ * ----------						-------------------------------------
+ * a = 1							a = 1 (no skip arrays)
+ * a = ANY('{0, 1}')				a = ANY('{0, 1}') (no skip arrays)
+ * b = 42							skip a AND b = 42
+ * b = ANY('{40, 42}')				skip a AND b = ANY('{40, 42}')
+ * a = 1 AND b = 42					a = 1 AND b = 42 (no skip arrays)
+ * a >= 1 AND b = 42				range skip a AND b = 42
+ * a = 1 AND b > 42					a = 1 AND b > 42 (no skip arrays)
+ * a >= 1 AND a <= 3 AND b = 42		range skip a AND b = 42
+ * a = 1 AND c <= 27				a = 1 AND skip b AND c <= 27
+ * a = 1 AND d >= 1					a = 1 AND skip b AND skip c AND d >= 1
+ * a = 1 AND b >= 42 AND d > 1		a = 1 AND range skip b AND skip c AND d > 1
+ */
+static int
+_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_skip = 1,
+				attno_inkey = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+#ifdef DEBUG_DISABLE_SKIP_SCAN
+	/* don't attempt to add skip arrays */
+	return numArrayKeys;
+#endif
+
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+			/* Look up input opclass's equality operator (might fail) */
+			skip_eq_ops[attno_skip - 1] =
+				get_opfamily_member(opfamily, opcintype, opcintype,
+									BTEqualStrategyNumber);
+			if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip array for the last input scan key's attribute -- even when
+		 * there are only inequality keys on that attribute.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Cannot keep adding skip arrays after a RowCompare
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys
+			 */
+			if (attno_has_equal)
+			{
+				/* Attributes with an = key must have InvalidOid eq_op set */
+				skip_eq_ops[attno_skip - 1] = InvalidOid;
+			}
+			else
+			{
+				Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+				Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+				/* Look up input opclass's equality operator (might fail) */
+				skip_eq_ops[attno_skip - 1] =
+					get_opfamily_member(opfamily, opcintype, opcintype,
+										BTEqualStrategyNumber);
+
+				if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+				{
+					/*
+					 * Input opclass lacks an equality strategy operator, so
+					 * don't generate a skip array that definitely won't work
+					 */
+					break;
+				}
+
+				/* plan on adding a backfill skip array for this attribute */
+				(*numSkipArrayKeys)++;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys include any equality strategy
+		 * scan keys (IS NULL keys count as equality keys here)
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+
+		/*
+		 * We don't support RowCompare transformation.  Remember that we saw a
+		 * RowCompare, so that we don't keep adding skip attributes.  (We may
+		 * still backfill skip attributes before the RowCompare, so that it
+		 * will be marked required later on.)
+		 */
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
 /*
  * _bt_find_extreme_element() -- get least or greatest array element
  *
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 80b04d6ca..815cbcfb7 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -31,6 +31,7 @@
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
 #include "storage/read_stream.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -76,14 +77,26 @@ typedef struct BTParallelScanDescData
 
 	/*
 	 * btps_arrElems is used when scans need to schedule another primitive
-	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
+	 * index scan with one or more SAOP arrays.  Holds BTArrayKeyInfo.cur_elem
+	 * offsets for each = scan key associated with a ScalarArrayOp array.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * Additional space (at the end of the struct) is used when scans need to
+	 * schedule another primitive index scan with one or more skip arrays.
+	 * Holds a flattened datum representation for each = scan key associated
+	 * with a skip array.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -541,10 +554,166 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume that every input scan key will be output with
+	 * its own SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * array (and an associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		CompactAttribute *attr;
+
+		/*
+		 * We make the conservative assumption that every index column will
+		 * also require a skip array.
+		 *
+		 * Every skip array must have space to store its scan key's sk_flags.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		/* Consider space required to store a datum of opclass input type */
+		attr = TupleDescCompactAttr(rel->rd_att, attnum - 1);
+		if (attr->attbyval)
+		{
+			/* This index attribute stores pass-by-value datums */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  true, attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * This index attribute stores pass-by-reference datums.
+		 *
+		 * Assume that serializing this array will use just as much space as a
+		 * pass-by-value datum, in addition to space for the largest possible
+		 * whole index tuple (this is not just a per-datum portion of the
+		 * largest possible tuple because that'd be almost as large anyway).
+		 *
+		 * This is quite conservative, but it's not clear how we could do much
+		 * better.  The executor requires an up-front storage request size
+		 * that reliably covers the scan's high watermark memory usage.  We
+		 * can't be sure of the real high watermark until the scan is over.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BTMaxItemSize);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   array->attbyval, array->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array key, while freeing memory allocated for old
+		 * sk_argument where required
+		 */
+		if (!array->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -613,6 +782,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -679,14 +849,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -831,6 +996,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -849,12 +1015,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
 	LWLockRelease(&btscan->btps_lock);
 }
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 3d46fb5df..1ef2cb2b5 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -983,7 +983,21 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * one we use --- by definition, they are either redundant or
 	 * contradictory.
 	 *
-	 * Any regular (not SK_SEARCHNULL) key implies a NOT NULL qualifier.
+	 * In practice we rarely see any "attribute boundary key gaps" here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  All
+	 * array keys are always considered = keys, but we'll sometimes need to
+	 * treat the current key value as if we were using an inequality strategy.
+	 * This happens with range skip arrays, which store inequality keys in the
+	 * array's low_compare/high_compare fields (used to find the first/last
+	 * set of matches, when = key will lack a usable sk_argument value).
+	 * These are always preferred over any redundant "standard" inequality
+	 * keys on the same column (per the usual rule about preferring = keys).
+	 * Note also that any column with an = skip array key can never have an
+	 * additional, contradictory = key.
+	 *
+	 * All keys (with the exception of SK_SEARCHNULL keys and SK_BT_SKIP
+	 * array keys whose array is "null_elem=true") imply a NOT NULL qualifier.
 	 * If the index stores nulls at the end of the index we'll be starting
 	 * from, and we have no boundary key for the column (which means the key
 	 * we deduced NOT NULL from is an inequality key that constrains the other
@@ -1040,8 +1054,54 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is MINVAL, choose low_compare (when scanning
+				 * backwards it'll be MAXVAL, and we'll choose high_compare).
+				 *
+				 * Note: if the array's low_compare key makes 'chosen' NULL,
+				 * then we behave as if the array's first element is -inf,
+				 * except when !array->null_elem implies a usable NOT NULL
+				 * constraint.
+				 */
+				if (chosen != NULL &&
+					(chosen->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skipequalitykey = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MAXVAL));
+						chosen = array->low_compare;
+					}
+					else
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MINVAL));
+						chosen = array->high_compare;
+					}
+
+					Assert(chosen == NULL ||
+						   chosen->sk_attno == skipequalitykey->sk_attno);
+
+					if (!array->null_elem)
+						impliesNN = skipequalitykey;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1084,9 +1144,40 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 
 				/*
-				 * Done if that was the last attribute, or if next key is not
-				 * in sequence (implying no boundary key is available for the
-				 * next attribute).
+				 * If the key that we just added to startKeys[] is a skip
+				 * array = key whose current element is marked NEXT or PRIOR,
+				 * make strat_total > or < (and stop adding boundary keys).
+				 * This can only happen with opclasses that lack skip support.
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(strat_total == BTEqualStrategyNumber);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We're done.  We'll never find an exact = match for a
+					 * NEXT or PRIOR sentinel sk_argument value.  There's no
+					 * sense in trying to add more keys to startKeys[].
+					 */
+					break;
+				}
+
+				/*
+				 * Done if that was the last scan key output by preprocessing.
+				 * Also done if there is a gap index attribute that lacks a
+				 * usable key (only possible when preprocessing was unable to
+				 * generate a skip array key to "fill in the gap").
 				 */
 				if (i >= so->numberOfKeys ||
 					cur->sk_attno != curattr + 1)
@@ -1581,31 +1672,10 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * We skip this for the first page read by each (primitive) scan, to avoid
 	 * slowing down point queries.  They typically don't stand to gain much
 	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
-	 *
-	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
-	 * just set a low-order required array's key to the best available match
-	 * for a truncated -inf attribute value from the prior page's high key
-	 * (array element 0 is always the best available match in this scenario).
-	 * It's quite likely that matches for array element 0 begin on this page,
-	 * but the start of matches won't necessarily align with page boundaries.
-	 * When the start of matches is somewhere in the middle of this page, it
-	 * would be wrong to treat page's final non-pivot tuple as representative.
-	 * Doing so might lead us to treat some of the page's earlier tuples as
-	 * being part of a group of tuples thought to satisfy the required keys.
-	 *
-	 * Note: Conversely, in the case where the scan's arrays just advanced
-	 * using the prior page's HIKEY _without_ advancement setting scanBehind,
-	 * the start of matches must be aligned with page boundaries, which makes
-	 * it safe to attempt the optimization here now.  It's also safe when the
-	 * prior page's HIKEY simply didn't need to advance any required array. In
-	 * both cases we can safely assume that the _first_ tuple from this page
-	 * must be >= the current set of array keys/equality constraints. And so
-	 * if the final tuple is == those same keys (and also satisfies any
-	 * required < or <= strategy scan keys) during the precheck, we can safely
-	 * assume that this must also be true of all earlier tuples from the page.
+	 * overhead of the precheck.  Also avoid it during scans with array keys,
+	 * which might be using skip scan (XXX fixed in next commit).
 	 */
-	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !arrayKeys && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 2aee9bbf6..0e6b8b3ab 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -30,6 +30,17 @@
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									  int32 set_elem_result, Datum tupdatum, bool tupnull);
+static void _bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_array_set_low_or_high(Relation rel, ScanKey skey,
+									  BTArrayKeyInfo *array, bool low_not_high);
+static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -207,6 +218,7 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 	int32		result = 0;
 
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
+	Assert(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)));
 
 	if (tupnull)				/* NULL tupdatum */
 	{
@@ -283,6 +295,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* SAOP arrays never have NULLs */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -405,6 +419,185 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, since skip arrays don't really
+ * contain elements (they generate their array elements procedurally instead).
+ * Our interface matches that of _bt_binsrch_array_skey in every other way.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  We return -1 when tupdatum/tupnull is lower that any value
+ * within the range of the array, and 1 when it is higher than every value.
+ * Caller should pass *set_elem_result to _bt_skiparray_set_element to advance
+ * the array.
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (array->null_elem)
+	{
+		Assert(!array->low_compare && !array->high_compare);
+
+		*set_elem_result = 0;
+		return;
+	}
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+
+	/*
+	 * Assert that any keys that were assumed to be satisfied already (due to
+	 * caller passing cur_elem_trig=true) really are satisfied as expected
+	 */
+#ifdef USE_ASSERT_CHECKING
+	if (cur_elem_trig)
+	{
+		if (ScanDirectionIsForward(dir) && array->low_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												  array->low_compare->sk_collation,
+												  tupdatum,
+												  array->low_compare->sk_argument)));
+
+		if (ScanDirectionIsBackward(dir) && array->high_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												  array->high_compare->sk_collation,
+												  tupdatum,
+												  array->high_compare->sk_argument)));
+	}
+#endif
+}
+
+/*
+ * _bt_skiparray_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Caller passes set_elem_result returned by _bt_binsrch_skiparray_skey for
+ * caller's tupdatum/tupnull.
+ *
+ * We copy tupdatum/tupnull into skey's sk_argument iff set_elem_result == 0.
+ * Otherwise, we set skey to either the lowest or highest value that's within
+ * the range of caller's skip array (whichever is the best available match to
+ * tupdatum/tupnull that is still within the range of the skip array according
+ * to _bt_binsrch_skiparray_skey/set_elem_result).
+ */
+static void
+_bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  int32 set_elem_result, Datum tupdatum, bool tupnull)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (set_elem_result)
+	{
+		/* tupdatum/tupnull is out of the range of the skip array */
+		Assert(!array->null_elem);
+
+		_bt_array_set_low_or_high(rel, skey, array, set_elem_result < 0);
+		return;
+	}
+
+	/* Advance skip array to tupdatum (or tupnull) value */
+	if (unlikely(tupnull))
+	{
+		_bt_skiparray_set_isnull(rel, skey, array);
+		return;
+	}
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen);
+}
+
+/*
+ * _bt_skiparray_set_isnull() -- set skip array scan key to NULL
+ */
+static void
+_bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->null_elem && !array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -414,29 +607,355 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_array_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_array_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  bool low_not_high)
+{
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting array to lowest element (according to low_compare) */
+		skey->sk_flags |= SK_BT_MINVAL;
+	}
+	else
+	{
+		/* Setting array to highest element (according to high_compare) */
+		skey->sk_flags |= SK_BT_MAXVAL;
+	}
+}
+
+/*
+ * _bt_array_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the minimum value within the range
+	 * of a skip array (often just -inf) is never decrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		/* Successfully "decremented" array */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly decrement sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Decrement" from NULL to the high_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->high_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup->decrement(rel, skey->sk_argument, &uflow);
+	if (unlikely(uflow))
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			_bt_skiparray_set_isnull(rel, skey, array);
+
+			/* Successfully "decremented" array to NULL */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	/* Successfully decremented array */
+	return true;
+}
+
+/*
+ * _bt_array_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MINVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the maximum value within the range
+	 * of a skip array (often just +inf) is never incrementable
+	 */
+	if (skey->sk_flags & SK_BT_MAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		/* Successfully "incremented" array */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly increment sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Increment" from NULL to the low_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->low_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup->increment(rel, skey->sk_argument, &oflow);
+	if (unlikely(oflow))
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			_bt_skiparray_set_isnull(rel, skey, array);
+
+			/* Successfully "incremented" array to NULL */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	/* Successfully incremented array */
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -452,6 +971,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -461,29 +981,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_array_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_array_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -507,7 +1028,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 }
 
 /*
- * _bt_rewind_nonrequired_arrays() -- Rewind non-required arrays
+ * _bt_rewind_nonrequired_arrays() -- Rewind SAOP arrays not marked required
  *
  * Called when _bt_advance_array_keys decides to start a new primitive index
  * scan on the basis of the current scan position being before the position
@@ -539,10 +1060,15 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
  *
  * Note: _bt_verify_arrays_bt_first is called by an assertion to enforce that
  * everybody got this right.
+ *
+ * Note: In practice almost all SAOP arrays are marked required during
+ * preprocessing (if necessary by generating skip arrays).  It is hardly ever
+ * truly necessary to call here, but consistently doing so is simpler.
  */
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -550,7 +1076,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -562,16 +1087,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_array_set_low_or_high(rel, cur, array,
+								  ScanDirectionIsForward(dir));
 	}
 }
 
@@ -696,9 +1215,77 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (likely(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))))
+		{
+			/* Scankey has a valid/comparable sk_argument value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0)
+			{
+				/*
+				 * Interpret result in a way that takes NEXT/PRIOR into
+				 * account
+				 */
+				if (cur->sk_flags & SK_BT_NEXT)
+					result = -1;
+				else if (cur->sk_flags & SK_BT_PRIOR)
+					result = 1;
+
+				Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP));
+			}
+		}
+		else
+		{
+			BTArrayKeyInfo *array = NULL;
+
+			/*
+			 * Current array element/array = scan key value is a sentinel
+			 * value that represents the lowest (or highest) possible value
+			 * that's still within the range of the array.
+			 *
+			 * Like _bt_first, we only see MINVAL keys during forwards scans
+			 * (and similarly only see MAXVAL keys during backwards scans).
+			 * Even if the scan's direction changes, we'll stop at some higher
+			 * order key before we can ever reach any MAXVAL (or MINVAL) keys.
+			 * (However, unlike _bt_first we _can_ get to keys marked either
+			 * NEXT or PRIOR, regardless of the scan's current direction.)
+			 */
+			Assert(ScanDirectionIsForward(dir) ?
+				   !(cur->sk_flags & SK_BT_MAXVAL) :
+				   !(cur->sk_flags & SK_BT_MINVAL));
+
+			/*
+			 * There are no valid sk_argument values in MINVAL/MAXVAL keys.
+			 * Check if tupdatum is within the range of skip array instead.
+			 */
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			_bt_binsrch_skiparray_skey(false, dir, tupdatum, tupnull,
+									   array, cur, &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum satisfies both low_compare and high_compare, so
+				 * it's time to advance the array keys.
+				 *
+				 * Note: It's possible that the skip array will "advance" from
+				 * its MINVAL (or MAXVAL) representation to an alternative,
+				 * logically equivalent representation of the same value: a
+				 * representation where the = key gets a valid datum in its
+				 * sk_argument.  This is only possible when low_compare uses
+				 * the >= strategy (or high_compare uses the <= strategy).
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1017,18 +1604,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1053,18 +1631,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -1080,14 +1649,22 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			bool		cur_elem_trig = (sktrig_required && ikey == sktrig);
 
 			/*
-			 * Binary search for closest match that's available from the array
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems == -1)
+				_bt_binsrch_skiparray_skey(cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * Binary search for the closest match from the SAOP array
+			 */
+			else
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 		}
 		else
 		{
@@ -1163,11 +1740,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+		if (array)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->num_elems == -1)
+			{
+				/* Skip array's new element is tupdatum (or MINVAL/MAXVAL) */
+				_bt_skiparray_set_element(rel, cur, array, result,
+										  tupdatum, tupnull);
+			}
+			else if (array->cur_elem != set_elem)
+			{
+				/* SAOP array's new element is set_elem datum */
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
 		}
 	}
 
@@ -1581,10 +2168,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -1914,6 +2502,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key uses one of several sentinel values.  We just
+		 * fall back on _bt_tuple_before_array_skeys when we see such a value.
+		 */
+		if (key->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL |
+							 SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
@@ -1939,6 +2541,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			else
 			{
 				Assert(key->sk_flags & SK_SEARCHNOTNULL);
+				Assert(!(key->sk_flags & SK_BT_SKIP));
 				if (!isNull)
 					continue;	/* tuple satisfies this qual */
 			}
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index dd6f5a15c..817ad358f 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -106,6 +106,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 8546366ee..a6dd8eab5 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("ordering equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (GetIndexAmRoutineByAmId(amoid, false)->amcanhash)
 	{
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index f279853de..4227ab1a7 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f23cfad71..244f48f4f 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 5b35debc8..bb46ed0c7 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -193,6 +193,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -214,6 +216,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5943,6 +5947,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -7001,6 +7091,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -7010,17 +7147,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
+	List	   *indexSkipQuals;
 	int			indexcol;
 	bool		eqQualHere;
-	bool		found_saop;
+	bool		found_row_compare;
+	bool		found_array;
 	bool		found_is_null_op;
+	bool		have_correlation = false;
 	double		num_sa_scans;
+	double		correlation = 0.0;
 	ListCell   *lc;
 
 	/*
@@ -7031,19 +7170,24 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * it's OK to count them in indexSelectivity, but they should not count
 	 * for estimating numIndexTuples.  So we must examine the given indexquals
 	 * to find out which ones count as boundary quals.  We rely on the
-	 * knowledge that they are given in index column order.
+	 * knowledge that they are given in index column order.  Note that nbtree
+	 * preprocessing can add skip arrays that act as leading '=' quals in the
+	 * absence of ordinary input '=' quals, so in practice _most_ input quals
+	 * are able to act as index bound quals (which we take into account here).
 	 *
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
+	 * If there's a SAOP or skip array in the quals, we'll actually perform up
+	 * to N index descents (not just one), but the underlying array key's
 	 * operator can be considered to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
+	indexSkipQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
-	found_saop = false;
+	found_row_compare = false;
+	found_array = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -7051,17 +7195,202 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
 		ListCell   *lc2;
 
-		if (indexcol != iclause->indexcol)
+		if (indexcol < iclause->indexcol)
 		{
-			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			double		num_sa_scans_prev_cols = num_sa_scans;
+
+			/*
+			 * Beginning of a new column's quals.
+			 *
+			 * Skip scans use skip arrays, which are ScalarArrayOp style
+			 * arrays that generate their elements procedurally and on demand.
+			 * Given a composite index on "(a, b)", and an SQL WHERE clause
+			 * "WHERE b = 42", a skip scan will effectively use an indexqual
+			 * "WHERE a = ANY('{every col a value}') AND b = 42".  (Obviously,
+			 * the array on "a" must also return "IS NULL" matches, since our
+			 * WHERE clause used no strict operator on "a").
+			 *
+			 * Here we consider how nbtree will backfill skip arrays for any
+			 * index columns that lacked an '=' qual.  This maintains our
+			 * num_sa_scans estimate, and determines if this new column (the
+			 * "iclause->indexcol" column, not the prior "indexcol" column)
+			 * can have its RestrictInfos/quals added to indexBoundQuals.
+			 *
+			 * We'll need to handle columns that have inequality quals, where
+			 * the skip array generates values from a range constrained by the
+			 * quals (not every possible value, never with IS NULL matching).
+			 * indexSkipQuals tracks the prior column's quals (that is, the
+			 * "indexcol" column's quals) to help us with this.
+			 */
+			if (found_row_compare)
+			{
+				/*
+				 * Skip arrays can't be added after a RowCompare input qual
+				 * due to limitations in nbtree
+				 */
+				break;
+			}
+			if (eqQualHere)
+			{
+				/*
+				 * Don't need to add a skip array for an indexcol that already
+				 * has an '=' qual/equality constraint
+				 */
+				indexcol++;
+				indexSkipQuals = NIL;
+			}
 			eqQualHere = false;
-			indexcol++;
+
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct;
+				bool		isdefault = true;
+
+				found_array = true;
+
+				/*
+				 * A skipped attribute's ndistinct forms the basis of our
+				 * estimate of the total number of "array elements" used by
+				 * its skip array at runtime.  Look that up first.
+				 */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+				if (indexcol == 0)
+				{
+					/*
+					 * Get an estimate of the leading column's correlation in
+					 * passing (avoids rereading variable stats below)
+					 */
+					if (HeapTupleIsValid(vardata.statsTuple))
+						correlation = btcost_correlation(index, &vardata);
+					have_correlation = true;
+				}
+
+				ReleaseVariableStats(vardata);
+
+				/*
+				 * If ndistinct is a default estimate, conservatively assume
+				 * that no skipping will happen at runtime
+				 */
+				if (isdefault)
+				{
+					num_sa_scans = num_sa_scans_prev_cols;
+					break;		/* done building indexBoundQuals */
+				}
+
+				/*
+				 * Apply indexcol's indexSkipQuals selectivity to ndistinct
+				 */
+				if (indexSkipQuals != NIL)
+				{
+					List	   *partialSkipQuals;
+					Selectivity ndistinctfrac;
+
+					/*
+					 * If the index is partial, AND the index predicate with
+					 * the index-bound quals to produce a more accurate idea
+					 * of the number of distinct values for prior indexcol
+					 */
+					partialSkipQuals = add_predicate_to_index_quals(index,
+																	indexSkipQuals);
+
+					ndistinctfrac = clauselist_selectivity(root, partialSkipQuals,
+														   index->rel->relid,
+														   JOIN_INNER,
+														   NULL);
+
+					/*
+					 * If ndistinctfrac is selective (on its own), the scan is
+					 * unlikely to benefit from repositioning itself using
+					 * later quals.  Do not allow iclause->indexcol's quals to
+					 * be added to indexBoundQuals (it would increase descent
+					 * costs, without lowering numIndexTuples costs by much).
+					 */
+					if (ndistinctfrac < DEFAULT_RANGE_INEQ_SEL)
+					{
+						num_sa_scans = num_sa_scans_prev_cols;
+						break;	/* done building indexBoundQuals */
+					}
+
+					/* Adjust ndistinct downward */
+					ndistinct = rint(ndistinct * ndistinctfrac);
+					ndistinct = Max(ndistinct, 1);
+				}
+
+				/*
+				 * When there's no inequality quals, account for the need to
+				 * find an initial value by counting -inf/+inf as a value.
+				 *
+				 * We don't charge anything extra for possible next/prior key
+				 * index probes, which are sometimes used to find the next
+				 * valid skip array element (ahead of using the located
+				 * element value to relocate the scan to the next position
+				 * that might contain matching tuples).  It seems hard to do
+				 * better here.  Use of the skip support infrastructure often
+				 * avoids most next/prior key probes.  But even when it can't,
+				 * there's a decent chance that most individual next/prior key
+				 * probes will locate a leaf page whose key space overlaps all
+				 * of the scan's keys (even the lower-order keys) -- which
+				 * also avoids the need for a separate, extra index descent.
+				 * Note also that these probes are much cheaper than non-probe
+				 * primitive index scans: they're reliably very selective.
+				 */
+				if (indexSkipQuals == NIL)
+					ndistinct += 1;
+
+				/*
+				 * Update num_sa_scans estimate by multiplying by ndistinct.
+				 *
+				 * We make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * expecting skipping to be helpful...
+				 */
+				num_sa_scans *= ndistinct;
+
+				/*
+				 * ...but back out of adding this latest group of 1 or more
+				 * skip arrays when num_sa_scans exceeds the total number of
+				 * index pages (revert to num_sa_scans from before indexcol).
+				 * This causes a sharp discontinuity in cost (as a function of
+				 * the indexcol's ndistinct), but that is representative of
+				 * actual runtime costs.
+				 *
+				 * Note that skipping is helpful when each primitive index
+				 * scan only manages to skip over 1 or 2 irrelevant leaf pages
+				 * on average.  Skip arrays bring savings in CPU costs due to
+				 * the scan not needing to evaluate indexquals against every
+				 * tuple, which can greatly exceed any savings in I/O costs.
+				 * This test is a test of whether num_sa_scans implies that
+				 * we're past the point where the ability to skip ceases to
+				 * lower the scan's costs (even qual evaluation CPU costs).
+				 */
+				if (index->pages < num_sa_scans)
+				{
+					num_sa_scans = num_sa_scans_prev_cols;
+					break;		/* done building indexBoundQuals */
+				}
+
+				indexcol++;
+				indexSkipQuals = NIL;
+			}
+
+			/*
+			 * Finished considering the need to add skip arrays to bridge an
+			 * initial eqQualHere gap between the old and new index columns
+			 * (or there was no initial eqQualHere gap in the first place).
+			 *
+			 * If an initial gap could not be bridged, then new column's quals
+			 * (i.e. iclause->indexcol's quals) won't go into indexBoundQuals,
+			 * and so won't affect our final numIndexTuples estimate.
+			 */
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+				break;			/* done building indexBoundQuals */
 		}
 
+		Assert(indexcol == iclause->indexcol);
+
 		/* Examine each indexqual associated with this index clause */
 		foreach(lc2, iclause->indexquals)
 		{
@@ -7081,6 +7410,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_row_compare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -7089,7 +7419,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				double		alength = estimate_array_length(root, other_operand);
 
 				clause_op = saop->opno;
-				found_saop = true;
+				found_array = true;
 				/* estimate SA descents by indexBoundQuals only */
 				if (alength > 1)
 					num_sa_scans *= alength;
@@ -7101,7 +7431,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skip scan purposes */
 					eqQualHere = true;
 				}
 			}
@@ -7120,19 +7450,28 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
+
+			/*
+			 * We apply inequality selectivities to estimate index descent
+			 * costs with scans that use skip arrays.  Save this indexcol's
+			 * RestrictInfos if it looks like they'll be needed for that.
+			 */
+			if (!eqQualHere && !found_row_compare &&
+				indexcol < index->nkeycolumns - 1)
+				indexSkipQuals = lappend(indexSkipQuals, rinfo);
 		}
 	}
 
 	/*
 	 * If index is unique and we found an '=' clause for each column, we can
 	 * just assume numIndexTuples = 1 and skip the expensive
-	 * clauselist_selectivity calculations.  However, a ScalarArrayOp or
-	 * NullTest invalidates that theory, even though it sets eqQualHere.
+	 * clauselist_selectivity calculations.  However, an array or NullTest
+	 * always invalidates that theory (even when eqQualHere has been set).
 	 */
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
-		!found_saop &&
+		!found_array &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
 	else
@@ -7154,7 +7493,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		numIndexTuples = btreeSelectivity * index->rel->tuples;
 
 		/*
-		 * btree automatically combines individual ScalarArrayOpExpr primitive
+		 * btree automatically combines individual array element primitive
 		 * index scans whenever the tuples covered by the next set of array
 		 * keys are close to tuples covered by the current set.  That puts a
 		 * natural ceiling on the worst case number of descents -- there
@@ -7172,16 +7511,18 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * of leaf pages (we make it 1/3 the total number of pages instead) to
 		 * give the btree code credit for its ability to continue on the leaf
 		 * level with low selectivity scans.
+		 *
+		 * Note: num_sa_scans includes both ScalarArrayOp array elements and
+		 * skip array elements whose qual affects our numIndexTuples estimate.
 		 */
 		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
-		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
-		 * to integer.
+		 * As in genericcostestimate(), we have to adjust for any array quals
+		 * included in indexBoundQuals, and then round to integer.
 		 *
-		 * It is tempting to make genericcostestimate behave as if SAOP
+		 * It is tempting to make genericcostestimate behave as if array
 		 * clauses work in almost the same way as scalar operators during
 		 * btree scans, making the top-level scan look like a continuous scan
 		 * (as opposed to num_sa_scans-many primitive index scans).  After
@@ -7214,7 +7555,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * comparisons to descend a btree of N leaf tuples.  We charge one
 	 * cpu_operator_cost per comparison.
 	 *
-	 * If there are ScalarArrayOpExprs, charge this once per estimated SA
+	 * If there are SAOP/skip array keys, charge this once per estimated SA
 	 * index descent.  The ones after the first one are not startup cost so
 	 * far as the overall plan goes, so just add them to "total" cost.
 	 */
@@ -7234,110 +7575,25 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * cost is somewhat arbitrarily set at 50x cpu_operator_cost per page
 	 * touched.  The number of such pages is btree tree height plus one (ie,
 	 * we charge for the leaf page too).  As above, charge once per estimated
-	 * SA index descent.
+	 * SAOP/skip array descent.
 	 */
 	descentCost = (index->tree_height + 1) * DEFAULT_PAGE_CPU_MULTIPLIER * cpu_operator_cost;
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* btcost_correlation already called earlier on */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..2bd35d2d2
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns skip support struct, allocating in caller's memory
+ * context.  Otherwise returns NULL, indicating that operator class has no
+ * skip support function.
+ */
+SkipSupport
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse)
+{
+	Oid			skipSupportFunction;
+	SkipSupport sksup;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return NULL;
+
+	sksup = palloc(sizeof(SkipSupportData));
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return sksup;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 9682f9dbd..347089b76 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2304,6 +2305,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 4f8402ef9..1b72059d2 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,6 +13,7 @@
 
 #include "postgres.h"
 
+#include <limits.h>
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
@@ -21,6 +22,7 @@
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -416,6 +418,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..3e6f30d74 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value that
+     might be stored in an index, so the domain of the particular data type
+     stored within the index (the input opclass type) must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index 768b77aa0..d5adb58c1 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -835,7 +835,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 0960f5ba9..5a369fdfb 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4261,7 +4261,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage
      <replaceable>column_name</replaceable> =
      <replaceable>value2</replaceable> ...</literal> construct, though only
     when the optimizer transforms the construct into an equivalent
-    multi-valued array representation.
+    multi-valued array representation.  Similarly, when B-Tree index scans use
+    the skip scan strategy, an index search is performed each time the scan is
+    repositioned to the next index leaf page that might have matching tuples.
    </para>
   </note>
   <tip>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index 387baac7e..d0470ac79 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -860,6 +860,37 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE thousand IN (1, 2, 3, 4);
     <structname>tenk1_thous_tenthous</structname> index leaf page.
    </para>
 
+   <para>
+    The <quote>Index Searches</quote> line is also useful with B-tree index
+    scans that apply the <firstterm>skip scan</firstterm> optimization to
+    more efficiently traverse through an index:
+<screen>
+EXPLAIN ANALYZE SELECT four, unique1 FROM tenk1 WHERE four BETWEEN 1 AND 3 AND unique1 = 42;
+                                                              QUERY PLAN
+-------------------------------------------------------------------&zwsp;---------------------------------------------------------------
+ Index Only Scan using tenk1_four_unique1_idx on tenk1  (cost=0.29..6.90 rows=1 width=8) (actual time=0.006..0.007 rows=1.00 loops=1)
+   Index Cond: ((four &gt;= 1) AND (four &lt;= 3) AND (unique1 = 42))
+   Heap Fetches: 0
+   Index Searches: 3
+   Buffers: shared hit=7
+ Planning Time: 0.029 ms
+ Execution Time: 0.012 ms
+</screen>
+
+    Here we see an Index-Only Scan node using
+    <structname>tenk1_four_unique1_idx</structname>, a composite index on the
+    <structname>tenk1</structname> table's <structfield>four</structfield> and
+    <structfield>unique1</structfield> columns.  The scan performs 3 searches
+    that each read a single index leaf page:
+    <quote><literal>four = 1 AND unique1 = 42</literal></quote>,
+    <quote><literal>four = 2 AND unique1 = 42</literal></quote>, and
+    <quote><literal>four = 3 AND unique1 = 42</literal></quote>.  This index
+    is generally a good target for skip scan, since its leading column (the
+    <structfield>four</structfield> column) contains only 4 distinct values,
+    while its second/final column (the <structfield>unique1</structfield>
+    column) contains many distinct values.
+   </para>
+
    <para>
     Another type of extra information is the number of rows removed by a
     filter condition:
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 053619624..7e23a7b6e 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index 0c274d56a..23bf33f10 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  ordering equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 8879554c3..bfb1a286e 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -581,6 +581,47 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Index Only Scan using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Index Only Scan Backward using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 --
 -- Test for multilevel page deletion
 --
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index bd5f002cf..15dde752f 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1637,7 +1637,9 @@ DROP TABLE syscol_table;
 -- Tests for IS NULL/IS NOT NULL with b-tree indexes
 --
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 SET enable_seqscan = OFF;
 SET enable_indexscan = ON;
@@ -1645,7 +1647,7 @@ SET enable_bitmapscan = ON;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1657,13 +1659,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1678,12 +1680,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1695,13 +1703,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1722,12 +1730,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0,
      1
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc nulls last,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1739,13 +1753,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1760,12 +1774,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2  nulls first,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1777,13 +1797,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1798,6 +1818,12 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 -- Check initial-positioning logic too
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2);
@@ -1829,20 +1855,24 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
 (2 rows)
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-         |        
-     278 |     999
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 5;
+ unique1 |  unique2   
+---------+------------
+     500 |           
+     100 |           
+         |           
+         | 2147483647
+     278 |        999
+(5 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-     278 |     999
-       0 |     998
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 3;
+ unique1 |  unique2   
+---------+------------
+         | 2147483647
+     278 |        999
+       0 |        998
+(3 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
@@ -2247,7 +2277,8 @@ SELECT count(*) FROM dupindexcols
 (1 row)
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 explain (costs off)
 SELECT unique1 FROM tenk1
@@ -2269,7 +2300,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2289,7 +2320,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2309,6 +2340,25 @@ ORDER BY thousand DESC, tenthous DESC;
         0 |     3000
 (2 rows)
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ Index Only Scan Backward using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > 995) AND (tenthous = ANY ('{998,999}'::integer[])))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+ thousand | tenthous 
+----------+----------
+      999 |      999
+      998 |      998
+(2 rows)
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -2339,6 +2389,45 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 ---------
 (0 rows)
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+                   QUERY PLAN                    
+-------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY (NULL::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY ('{NULL,NULL,NULL}'::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: ((unique1 IS NULL) AND (unique1 IS NULL))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+ unique1 
+---------
+(0 rows)
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
                                 QUERY PLAN                                 
@@ -2462,6 +2551,44 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 ---------
 (0 rows)
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > '-1'::integer) AND (thousand >= 0) AND (tenthous = 3000))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+(1 row)
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand < 3) AND (thousand <= 2) AND (tenthous = 1001))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        1 |     1001
+(1 row)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index b1d12585e..cf48ae6d0 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5332,9 +5332,10 @@ Function              | in_range(time without time zone,time without time zone,i
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql
index 670ad5c6e..68c61dbc7 100644
--- a/src/test/regress/sql/btree_index.sql
+++ b/src/test/regress/sql/btree_index.sql
@@ -327,6 +327,27 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 
 --
 -- Test for multilevel page deletion
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index be570da08..40ba3d65b 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -650,7 +650,9 @@ DROP TABLE syscol_table;
 --
 
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 
 SET enable_seqscan = OFF;
@@ -663,6 +665,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -675,6 +678,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NUL
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0, 1);
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -686,6 +690,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -697,6 +702,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -716,9 +722,9 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
   ORDER BY unique2 LIMIT 2;
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 5;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 3;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
 
@@ -852,7 +858,8 @@ SELECT count(*) FROM dupindexcols
   WHERE f1 BETWEEN 'WA' AND 'ZZZ' and id < 1000 and f1 ~<~ 'YX';
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 
 explain (costs off)
@@ -864,7 +871,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -874,7 +881,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -884,6 +891,15 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand DESC, tenthous DESC;
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -897,6 +913,21 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 
 SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('{33, 44}'::bigint[]);
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
 
@@ -942,6 +973,26 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
 SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3fbf5a4c2..8b8a04250 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -223,6 +223,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2737,6 +2738,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.49.0

#84Alena Rybakina
a.rybakina@postgrespro.ru
In reply to: Peter Geoghegan (#83)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Hi!

On 26.03.2025 02:45, Peter Geoghegan wrote:

On Sat, Mar 22, 2025 at 1:47 PM Peter Geoghegan<pg@bowt.ie> wrote:

Attached is v30, which fully replaces the pstate.prechecked
optimization with the new _bt_skip_ikeyprefix optimization (which now
appears in v30-0002-Lower-nbtree-skip-array-maintenance-overhead.patch,
and not in 0003-*, due to my committing the primscan scheduling patch
just now).

Attached is v31, which has a much-improved _bt_skip_ikeyprefix (which
I've once again renamed, this time to _bt_set_startikey).

I reviewed your first patch and noticed that you added the ability to
define new quals if the first column isn't usedin the query.

I replied an example like this:

CREATE TABLE sales ( id SERIAL PRIMARY KEY, region TEXT, product TEXT,
year INT );

INSERT INTO sales (region, product, year) SELECT CASE WHEN i % 4 <= 1
THEN 'North' WHEN i % 4 <= 2 THEN 'South' WHEN i % 4 <= 3 THEN 'East'
ELSE 'West' END, CASE WHEN random() > 0.5 THEN 'Laptop' ELSE 'Phone'
END, 2023 FROM generate_series(1, 100000) AS i;

vacuum analyze;

CREATE INDEX sales_idx ON sales (region, product, year);

EXPLAIN ANALYZE SELECT * FROM sales WHERE product = 'Laptop' AND year =
2023;

master gives the query plan with SeqScan:

QUERY PLAN
---------------------------------------------------------------------------------------------------------------
Seq Scan on sales (cost=0.00..2137.00 rows=49703 width=19) (actual
time=0.035..31.438 rows=50212.00 loops=1) Filter: ((product =
'Laptop'::text) AND (year = 2023)) Rows Removed by Filter: 49788
Buffers: shared hit=637 Planning: Buffers: shared hit=35 Planning Time:
0.695 ms Execution Time: 33.942 ms (8 rows)

Your patch sets fair costs for it and it helps take into account index
scan path and in my opinion it is perfect!)

QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on sales (cost=652.46..2031.86 rows=49493 width=19)
(actual time=18.039..33.723 rows=49767.00 loops=1) Recheck Cond:
((product = 'Laptop'::text) AND (year = 2023)) Heap Blocks: exact=637
Buffers: shared hit=642 read=50 -> Bitmap Index Scan on sales_idx
(cost=0.00..640.09 rows=49493 width=0) (actual time=17.756..17.756
rows=49767.00 loops=1) Index Cond: ((product = 'Laptop'::text) AND (year
= 2023)) Index Searches: 4 Buffers: shared hit=5 read=50 Planning:
Buffers: shared hit=55 read=1 Planning Time: 0.984 ms Execution Time:
36.940 ms

I think it would be useful to show information that we used an index
scan but at the same time we skipped the "region" column and I assume we
should output how many distinct values the "region" column had.

For example it will look like this "*Skip Scan on region (4 distinct
values)"*:

QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on sales (cost=652.46..2031.86 rows=49493 width=19)
(actual time=18.039..33.723 rows=49767.00 loops=1) Recheck Cond:
((product = 'Laptop'::text) AND (year = 2023)) Heap Blocks: exact=637
Buffers: shared hit=642 read=50 -> Bitmap Index Scan on sales_idx
(cost=0.00..640.09 rows=49493 width=0) (actual time=17.756..17.756
rows=49767.00 loops=1) Index Cond: ((product = 'Laptop'::text) AND (year
= 2023)) *Skip Scan on region (4 distinct values)* Index Searches: 4
Buffers: shared hit=5 read=50 Planning: Buffers: shared hit=55 read=1
Planning Time: 0.984 ms Execution Time: 36.940 ms

What do you think?

I didn't see any regression tests. Maybe we should add some tests? To be
honest I didn't see it mentioned in the commit message but I might have
missed something.

--
Regards,
Alena Rybakina
Postgres Professional

In reply to: Alena Rybakina (#84)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Thu, Mar 27, 2025 at 6:03 PM Alena Rybakina
<a.rybakina@postgrespro.ru> wrote:

I replied an example like this:

This example shows costs that are dominated by heap access costs. Both
the sequential scan and the bitmap heap scan must access 637 heap
blocks. So I don't think that this is such a great example -- the heap
accesses are irrelevant from the point of view of assessing how well
we're modelling index scan related costs.

I think it would be useful to show information that we used an index scan but at the same time we skipped the "region" column and I assume we should output how many distinct values the "region" column had.

For example it will look like this "Skip Scan on region (4 distinct values)":

What do you think?

As I said on our call today, I think that we should keep the output
for EXPLAIN ANALYZE simple. While I'm sympathetic to the idea that we
should show more information about how quals can be applied in index
scan node output, that seems like it should be largely independent
work to me.

Masahiro Ikeda wrote a patch that aimed to improve matters in this
area some months back. I'm supportive of that (there is definitely
value in signalling to users that the index might actually look quite
different to how they imagine it looks, say by having an
omitted-by-query prefix attribute). I don't exactly know what the most
useful kind of information to show is with skip scan in place, since
skip scan makes the general nature of quals (whether a given qual is
what oracle calls "access predicates", or what oracle calls "filter
predicates") is made squishy/dynamic by skip scan, in a way that is
new.

The relationship between the number of values that a skip array ever
uses, and the number of primitive index scans is quite complicated.
Sometimes it is actually as simple as your example query, but that's
often not true. "Index Searches: N" can be affected by:

* The use of SAOP arrays, which also influence primitive scan
scheduling, in the same way as they have since Postgres 17 -- and can
be mixed freely with skip arrays.

* The availability of opclass skipsupport, which makes skip arrays
generate their element values by addition/subtraction from the current
array element, rather than using NEXT/PRIOR sentinel keys.

The sentinel keys act as probes that get the next real (non-sentinel)
value that we need to look up next. Whereas skip support can often
successfully guess that (for example) the next value in the index
after 268 is 269, saving a primitive scan each time (this might not
happen at all, or it might work only some of the time, or it might
work all of the time).

* Various primitive index scan scheduling heuristics.

Another concern here is that I don't want to invent a special kind of
"index search" just for skip scan. We're going to show an "Index
Searches: N" that's greater than 1 with SAOP array keys, too -- which
don't use skip scan at all (nothing new about that, except for the
fact that we report the number of searches directly from EXPLAIN
ANALYZE in Postgres 18). There really is almost no difference between
a scan with a skip array and a scan of the same index with a similar
SAOP array (when each array "contains the same elements", and is used
to scan the same index, in the same way). That's why the cost model is
as similar as possible to the Postgres 17 costing of SAOP array scans
-- it's really the same access method. Reusing the cost model makes
sense because actual execution times are almost identical when we
compare a skip array to a SAOP array in the way that I described.

The only advantage that I see from putting something about "skip scan"
in EXPLAIN ANALYZE is that it is more googleable that way. But it
seems like "Index Searches: N" is almost as good, most of the time. In
any case, the fact that we don't need a separate optimizer index
path/executor node for this is something that I see as a key
advantage, and something that I'd like EXPLAIN ANALYZE to preserve.

The problem with advertising that an index scan node is a skip scan
is: what if it just never skips? Never skipping like this isn't
necessarily unexpected. And even if it is unexpected, it's not
necessarily a problem.

I didn't see any regression tests. Maybe we should add some tests? To be honest I didn't see it mentioned in the commit message but I might have missed something.

There are definitely new regression tests -- I specifically tried to
keep the test coverage high, using gcov html reports (like the ones
from coverage.postgresql.org). The test updates appear towards the end
of the big patch file, though. Maybe you're just not used to seeing
tests appear last like this?

I use "git config diff.orderfile ... " to get this behavior. I find it
useful to put the important changes (particularly header file changes)
first, and less important changes (like tests) much later.

Thanks for taking a look at my patch!
--
Peter Geoghegan

In reply to: Peter Geoghegan (#83)
6 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Mar 25, 2025 at 7:45 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v31, which has a much-improved _bt_skip_ikeyprefix (which
I've once again renamed, this time to _bt_set_startikey).

Attached is v32, which has very few changes, but does add a new patch:
a patch that adds skip-array-specific primitive index scan heuristics
to _bt_advance_array_keys (this is
v32-0003-Improve-skip-scan-primitive-scan-scheduling.patch).

I feel compelled to do something about cases like the ones highlighted
by the attached test case, heikki-testcase-variant.sql. This shows how
the new heuristic added by my recent commit 9a2e2a28 can still
sometimes fail to ever be reached, in cases rather like the ones that
that was supposed to address [1]/messages/by-id/aa55adf3-6466-4324-92e6-5ef54e7c3918@iki.fi. This test case I'm posting now is
based on Heikki's adversarial test case (the one he posted back in
January). I've pushed it a bit further, demonstrating a remaining need
to tweak the primscan scheduling heuristics in light of skip scan. If
you run heikki-testcase-variant.sql against a patched server, without
v32-0003-Improve-skip-scan-primitive-scan-scheduling.patch, you'll see
exactly what I'm concerned about .

I am somewhat breaking my own rule about not inventing heuristics that
specifically care about which type of array (skip vs SAOP array) are
involved here. In my defense, the new heuristic isn't particularly
likely to influence primscan scheduling. It will seldom be needed or
have any noticeable influence, but just might be crucial with cases
like the one from the test case. It seems a little too hard for skip
scans to actually get the behavior from commit 9a2e2a28 -- which is
something that we really shouldn't leave to chance.

The plan around committing this work hasn't changed: I still intend to
commit everything next Wednesday or Thursday. Hope to get a review of
the new scan heuristic before then, but it's a small adjunct to what I
did in commit 9a2e2a28, so not too concerned about it adding risk.

Thanks

[1]: /messages/by-id/aa55adf3-6466-4324-92e6-5ef54e7c3918@iki.fi

--
Peter Geoghegan

Attachments:

heikki-testcase-variant.sqlapplication/sql; name=heikki-testcase-variant.sqlDownload
v32-0004-Apply-low-order-skip-key-in-_bt_first-more-often.patchapplication/x-patch; name=v32-0004-Apply-low-order-skip-key-in-_bt_first-more-often.patchDownload
From e2db23089c752e79095dc10a2cefe2815c863636 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 13 Jan 2025 16:08:32 -0500
Subject: [PATCH v32 4/5] Apply low-order skip key in _bt_first more often.

Convert low_compare and high_compare nbtree skip array inequalities
(with opclasses that offer skip support) such that _bt_first is
consistently able to use later keys when descending the tree within
_bt_first.

For example, an index qual "WHERE a > 5 AND b = 2" is now converted to
"WHERE a >= 6 AND b = 2" by a new preprocessing step that takes place
after a final low_compare and/or high_compare are chosen by all earlier
preprocessing steps.  That way the scan's initial call to _bt_first will
use "WHERE a >= 6 AND b = 2" to find the initial leaf level position,
rather than merely using "WHERE a > 5" -- "b = 2" can always be applied.
This has a decent chance of making the scan avoid an extra _bt_first
call that would otherwise be needed just to determine the lowest-sorting
"a" value in the index (the lowest that still satisfies "WHERE a > 5").

The transformation process can only lower the total number of index
pages read when the use of a more restrictive set of initial positioning
keys in _bt_first actually allows the scan to land on some later leaf
page directly, relative to the unoptimized case (or on an earlier leaf
page directly, when scanning backwards).  The savings can be far greater
when affected skip arrays come after some higher-order array.  For
example, a qual "WHERE x IN (1, 2, 3) AND y > 5 AND z = 2" can now save
as many as 3 _bt_first calls as a result of these transformations (there
can be as many as 1 _bt_first call saved per "x" array element).

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Discussion: https://postgr.es/m/CAH2-Wz=FJ78K3WsF3iWNxWnUCY9f=Jdg3QPxaXE=uYUbmuRz5Q@mail.gmail.com
---
 src/backend/access/nbtree/nbtpreprocesskeys.c | 180 ++++++++++++++++++
 src/test/regress/expected/create_index.out    |  21 ++
 src/test/regress/sql/create_index.sql         |  10 +
 3 files changed, 211 insertions(+)

diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 58b8c83cd..91d34ef96 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -50,6 +50,12 @@ static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
 								 BTArrayKeyInfo *array, bool *qual_ok);
 static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
 								 BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+									   BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
@@ -1295,6 +1301,171 @@ _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
 	return true;
 }
 
+/*
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts their
+ * operator strategies, while changing the operator as appropriate).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value MINVAL, using a transformed >= key
+ * instead of using the original > key makes it safe to include lower-order
+ * scan keys in the insertion scan key (there must be lower-order scan keys
+ * after the skip array).  We will avoid an extra _bt_first to find the first
+ * value in the index > sk_argument -- at least when the first real matching
+ * value in the index happens to be an exact match for the sk_argument value
+ * that we produced here by incrementing the original input key's sk_argument.
+ * (Backwards scans derive the same benefit when they encounter the sentinel
+ * value MAXVAL, by converting the high_compare key from < to <=.)
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01'.
+ *
+ * Note: We can safely modify array->low_compare/array->high_compare in place
+ * because they just point to copies of our scan->keyData[] input scan keys
+ * (namely the copies returned by _bt_preprocess_array_keys to be used as
+ * input into the standard preprocessing steps in _bt_preprocess_keys).
+ * Everything will be reset if there's a rescan.
+ */
+static void
+_bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+						   BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	/*
+	 * Called last among all preprocessing steps, when the skip array's final
+	 * low_compare and high_compare have both been chosen
+	 */
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1 && !array->null_elem && array->sksup);
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skiparray_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skiparray_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1832,6 +2003,15 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && array->sksup &&
+						!array->null_elem)
+						_bt_skiparray_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 15dde752f..fa4517e2d 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2589,6 +2589,27 @@ ORDER BY thousand;
         1 |     1001
 (1 row)
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+                                            QUERY PLAN                                            
+--------------------------------------------------------------------------------------------------
+ Limit
+   ->  Index Only Scan using tenk1_thous_tenthous on tenk1
+         Index Cond: ((thousand > '-1'::integer) AND (tenthous = ANY ('{1001,3000}'::integer[])))
+(3 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+        1 |     1001
+(2 rows)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index 40ba3d65b..e8c16959a 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -993,6 +993,16 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
 ORDER BY thousand;
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
-- 
2.49.0

v32-0003-Improve-skip-scan-primitive-scan-scheduling.patchapplication/x-patch; name=v32-0003-Improve-skip-scan-primitive-scan-scheduling.patchDownload
From 487b1723888a3a95effbebbb1670df614c16b639 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 26 Mar 2025 18:21:27 -0400
Subject: [PATCH v32 3/5] Improve skip scan primitive scan scheduling.

Fixes a few remaining cases where affected skip scans never quite manage
to reach the point of being able to apply the "passed first page"
heuristic added by commit 9a2e2a28.  They only need to manage to get
there once to converge on full index scan behavior, but it was still
possible for that to never happen, with the wrong workload.

Author: Peter Geoghegan <pg@bowt.ie>
---
 src/include/access/nbtree.h           |  3 +-
 src/backend/access/nbtree/nbtsearch.c |  1 +
 src/backend/access/nbtree/nbtutils.c  | 46 +++++++++++++++++++++++----
 3 files changed, 43 insertions(+), 7 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index c8708f2fd..02da56995 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1118,10 +1118,11 @@ typedef struct BTReadPageState
 
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
-	 * (only used during scans with array keys)
+	 * and primscan scheduling (only used during scans with array keys)
 	 */
 	int16		rechecks;
 	int16		targetdistance;
+	int16		nskipadvances;
 
 } BTReadPageState;
 
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 08cfbc45f..cec6676cc 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1655,6 +1655,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.continuescan = true; /* default assumption */
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
+	pstate.nskipadvances = 0;
 
 	if (ScanDirectionIsForward(dir))
 	{
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 01beaa4c8..8f9e6050d 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -26,6 +26,7 @@
 
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
+#define NSKIPADVANCES_THRESHOLD			3
 
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
@@ -41,7 +42,8 @@ static void _bt_array_set_low_or_high(Relation rel, ScanKey skey,
 									  BTArrayKeyInfo *array, bool low_not_high);
 static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
-static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
+static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir,
+											 bool *skip_array_set);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 										 IndexTuple tuple, TupleDesc tupdesc, int tupnatts,
@@ -969,7 +971,8 @@ _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
  * advanced (every array remains at its final element for scan direction).
  */
 static bool
-_bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
+_bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir,
+								 bool *skip_array_set)
 {
 	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -984,6 +987,9 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 		BTArrayKeyInfo *array = &so->arrayKeys[i];
 		ScanKey		skey = &so->keyData[array->scan_key];
 
+		if (array->num_elems == -1)
+			*skip_array_set = true;
+
 		if (ScanDirectionIsForward(dir))
 		{
 			if (_bt_array_increment(rel, skey, array))
@@ -1459,6 +1465,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	ScanDirection dir = so->currPos.dir;
 	int			arrayidx = 0;
 	bool		beyond_end_advance = false,
+				skip_array_advanced = false,
 				has_required_opposite_direction_only = false,
 				all_required_satisfied = true,
 				all_satisfied = true;
@@ -1755,6 +1762,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 				/* Skip array's new element is tupdatum (or MINVAL/MAXVAL) */
 				_bt_skiparray_set_element(rel, cur, array, result,
 										  tupdatum, tupnull);
+				skip_array_advanced = true;
 			}
 			else if (array->cur_elem != set_elem)
 			{
@@ -1771,11 +1779,19 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * higher-order arrays (might exhaust all the scan's arrays instead, which
 	 * ends the top-level scan).
 	 */
-	if (beyond_end_advance && !_bt_advance_array_keys_increment(scan, dir))
+	if (beyond_end_advance &&
+		!_bt_advance_array_keys_increment(scan, dir, &skip_array_advanced))
 		goto end_toplevel_scan;
 
 	Assert(_bt_verify_keys_with_arraykeys(scan));
 
+	/*
+	 * Maintain a page-level count of the number of times the scan's array
+	 * keys advanced in a way that affected at least one skip array
+	 */
+	if (sktrig_required && skip_array_advanced)
+		pstate->nskipadvances++;
+
 	/*
 	 * Does tuple now satisfy our new qual?  Recheck with _bt_check_compare.
 	 *
@@ -2058,13 +2074,31 @@ new_prim_scan:
 	 * This will in turn encourage _bt_readpage to apply the pstate.startikey
 	 * optimization more often.
 	 *
-	 * Note: This heuristic isn't as aggressive as you might think.  We're
+	 * Also continue the ongoing primitive index scan when it is still on the
+	 * first page if there have been more than NSKIPADVANCES_THRESHOLD calls
+	 * here that each advanced at least one of the scan's skip arrays
+	 * (deliberately ignore advancements that only affected SAOP arrays here).
+	 * A page that cycles through this many skip array elements is quite
+	 * likely to neighbor similar pages, that we'll also need to read.
+	 *
+	 * Note: These heuristics aren't as aggressive as you might think.  We're
 	 * conservative about allowing a primitive scan to step from the first
 	 * leaf page it reads to the page's sibling page (we only allow it on
-	 * first pages whose finaltup strongly suggests that it'll work out).
+	 * first pages whose finaltup strongly suggests that it'll work out, as
+	 * well as first pages that have a large number of skip array advances).
 	 * Clearing this first page finaltup hurdle is a strong signal in itself.
+	 *
+	 * Note: The NSKIPADVANCES_THRESHOLD heuristic exists only to avoid
+	 * pathological cases: cases where a skip scan should just behave like a
+	 * traditional full index scan, but ends up "skipping" again and again,
+	 * descending to the prior leaf page's direct sibling leaf page each time.
+	 * This misbehavior would otherwise be possible during scans that never
+	 * quite manage to "clear the first page finaltup hurdle".  Workloads like
+	 * this (that are at risk of never converging on full index scan behavior)
+	 * aren't very common, but they are a real concern.  No individual scan
+	 * should ever be allowed to do the wrong thing ceaselessly.
 	 */
-	if (!pstate->firstpage)
+	if (!pstate->firstpage || pstate->nskipadvances > NSKIPADVANCES_THRESHOLD)
 	{
 		/* Schedule a recheck once on the next (or previous) page */
 		so->scanBehind = true;
-- 
2.49.0

v32-0002-Enhance-nbtree-tuple-scan-key-optimizations.patchapplication/x-patch; name=v32-0002-Enhance-nbtree-tuple-scan-key-optimizations.patchDownload
From d63e4709874ce9dc9074814d7707948d90cbae32 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v32 2/5] Enhance nbtree tuple scan key optimizations.

Postgres 17 commit e0b1ee17 added a pair of closely related nbtree
optimizations: the "prechecked" and "firstpage" optimizations.  Both
optimizations avoided needlessly evaluating keys that are guaranteed to
be satisfied by applying page-level context.  These optimizations were
adapted to work with the nbtree ScalarArrayOp execution patch a few
months later, which became commit 5bf748b8.

The "prechecked" design had a number of notable weak points.  It didn't
account for the fact that an = array scan key's sk_argument field might
need to advance at the point of the page precheck (it didn't check the
precheck tuple against the key's array, only the key's sk_argument,
which needlessly made it ineffective in corner cases involving stepping
to a page having advanced the scan's arrays using a truncated high key).
It was also an "all or nothing" optimization: either it was completely
effective (skipping all required-in-scan-direction keys against all
attributes) for the whole page, or it didn't work at all.  This also
implied that it couldn't be used on pages where the scan had to
terminate before reaching the end of the page due to an unsatisfied
low-order key setting continuescan=false.

Replace both optimizations with a new optimization without any of these
weak points.  This works by giving affected _bt_readpage calls a scankey
offset that its _bt_checkkeys calls start at (an offset to the first key
that might not be satisfied by every non-pivot tuple from the page).
The new optimization is activated at the same point as the previous
"prechecked" optimization (at the start of a _bt_readpage of any page
after the scan's first).

While the "prechecked" optimization worked off of the highest non-pivot
tuple on the page (or the lowest, when scanning backwards), the new
"startikey" optimization always works off of a pair of non-pivot tuples
(the lowest and the highest, taken together).  Using a pair of scan keys
like this allows the "startikey" optimization to work seamlessly with
all kinds of scan keys, including = array keys, as well as scalar
inequalities (required in either scan direction).

Although this is independently useful work, the main motivation is to
fix regressions in index scans that are nominally eligible to use skip
scan, but can never actually benefit from skipping.  These are cases
where a leading prefix column contains many distinct values, especially
when the number of values approaches the total number of index tuples,
where skipping can never be profitable.  The CPU costs of skip array
maintenance is by far the main cost that must be kept under control.

Some scans with array keys (including all scans with skip arrays) will
temporarily cease fully maintaining the scan's arrays when the
optimization kicks in.  _bt_checkkeys will treat the scan's keys as if
they were not marked as required during preprocessing, for the duration
of an affected _bt_readpage call.  This relies on the non-required array
logic that was added to Postgres 17 by commit 5bf748b8.

Skip scan's approach of adding skip arrays during preprocessing and then
fixing (or significantly ameliorating) the resulting regressions seen in
unsympathetic cases is enabled by the optimization added by this commit
(and by the "look ahead" optimization introduced by commit 5bf748b8).
This allows the planner to avoid generating distinct, competing index
paths (one path for skip scan, another for an equivalent traditional
full index scan).  The overall effect is to make scan runtime close to
optimal, even when the planner works off an incorrect cardinality
estimate.  Scans will also perform well given a skipped column with data
skew: individual groups of pages with many distinct values in respect of
a skipped column can be read about as efficiently as before, without
having to give up on skipping over other provably-irrelevant leaf pages.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WznWDK45JfNPNvDxh6RQy-TaCwULaM5u5ALMXbjLBMcugQ@mail.gmail.com
---
 src/include/access/nbtree.h                   |  11 +-
 src/backend/access/nbtree/nbtpreprocesskeys.c |   1 +
 src/backend/access/nbtree/nbtree.c            |   1 +
 src/backend/access/nbtree/nbtsearch.c         |  63 +-
 src/backend/access/nbtree/nbtutils.c          | 590 ++++++++++++++----
 5 files changed, 512 insertions(+), 154 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index b86bf7bf3..c8708f2fd 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1059,6 +1059,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Check scan not still behind on next page? */
 	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
@@ -1105,6 +1106,8 @@ typedef struct BTReadPageState
 	IndexTuple	finaltup;		/* Needed by scans with array keys */
 	Page		page;			/* Page being read */
 	bool		firstpage;		/* page is first for primitive scan? */
+	bool		forcenonrequired;	/* treat all keys as nonrequired? */
+	int			startikey;		/* start comparisons from this scan key */
 
 	/* Per-tuple input parameters, set by _bt_readpage for _bt_checkkeys */
 	OffsetNumber offnum;		/* current tuple's page offset number */
@@ -1113,13 +1116,6 @@ typedef struct BTReadPageState
 	OffsetNumber skip;			/* Array keys "look ahead" skip offnum */
 	bool		continuescan;	/* Terminate ongoing (primitive) index scan? */
 
-	/*
-	 * Input and output parameters, set and unset by both _bt_readpage and
-	 * _bt_checkkeys to manage precheck optimizations
-	 */
-	bool		prechecked;		/* precheck set continuescan to 'true'? */
-	bool		firstmatch;		/* at least one match so far?  */
-
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
 	 * (only used during scans with array keys)
@@ -1327,6 +1323,7 @@ extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arra
 						  IndexTuple tuple, int tupnatts);
 extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
 									 IndexTuple finaltup);
+extern void _bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 75b9e7365..58b8c83cd 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -1360,6 +1360,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	 * (also checks if we should add extra skip arrays based on input keys)
 	 */
 	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
+	so->skipScan = (numSkipArrayKeys > 0);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 815cbcfb7..7d3ce6ecb 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -349,6 +349,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	else
 		so->keyData = NULL;
 
+	so->skipScan = false;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->oppositeDirCheck = false;
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 1ef2cb2b5..08cfbc45f 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1648,47 +1648,14 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.finaltup = NULL;
 	pstate.page = page;
 	pstate.firstpage = firstpage;
+	pstate.forcenonrequired = false;
+	pstate.startikey = 0;
 	pstate.offnum = InvalidOffsetNumber;
 	pstate.skip = InvalidOffsetNumber;
 	pstate.continuescan = true; /* default assumption */
-	pstate.prechecked = false;
-	pstate.firstmatch = false;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
-	/*
-	 * Prechecking the value of the continuescan flag for the last item on the
-	 * page (for backwards scan it will be the first item on a page).  If we
-	 * observe it to be true, then it should be true for all other items. This
-	 * allows us to do significant optimizations in the _bt_checkkeys()
-	 * function for all the items on the page.
-	 *
-	 * With the forward scan, we do this check for the last item on the page
-	 * instead of the high key.  It's relatively likely that the most
-	 * significant column in the high key will be different from the
-	 * corresponding value from the last item on the page.  So checking with
-	 * the last item on the page would give a more precise answer.
-	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.  Also avoid it during scans with array keys,
-	 * which might be using skip scan (XXX fixed in next commit).
-	 */
-	if (!pstate.firstpage && !arrayKeys && minoff < maxoff)
-	{
-		ItemId		iid;
-		IndexTuple	itup;
-
-		iid = PageGetItemId(page, ScanDirectionIsForward(dir) ? maxoff : minoff);
-		itup = (IndexTuple) PageGetItem(page, iid);
-
-		/* Call with arrayKeys=false to avoid undesirable side-effects */
-		_bt_checkkeys(scan, &pstate, false, itup, indnatts);
-		pstate.prechecked = pstate.continuescan;
-		pstate.continuescan = true; /* reset */
-	}
-
 	if (ScanDirectionIsForward(dir))
 	{
 		/* SK_SEARCHARRAY forward scans must provide high key up front */
@@ -1716,6 +1683,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
+		/*
+		 * Consider pstate.startikey optimization once the ongoing primitive
+		 * index scan has already read at least one page
+		 */
+		if (!pstate.firstpage && minoff < maxoff)
+			_bt_set_startikey(scan, &pstate);
+
 		/* load items[] in ascending order */
 		itemIndex = 0;
 
@@ -1752,6 +1726,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum < pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1761,7 +1736,6 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			if (passes_quals)
 			{
 				/* tuple passes all scan key conditions */
-				pstate.firstmatch = true;
 				if (!BTreeTupleIsPosting(itup))
 				{
 					/* Remember it */
@@ -1816,7 +1790,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			int			truncatt;
 
 			truncatt = BTreeTupleGetNAtts(itup, rel);
-			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
+			pstate.forcenonrequired = false;
+			pstate.startikey = 0;
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -1855,6 +1830,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
+		/*
+		 * Consider pstate.startikey optimization once the ongoing primitive
+		 * index scan has already read at least one page
+		 */
+		if (!pstate.firstpage && minoff < maxoff)
+			_bt_set_startikey(scan, &pstate);
+
 		/* load items[] in descending order */
 		itemIndex = MaxTIDsPerBTreePage;
 
@@ -1894,6 +1876,11 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (offnum == minoff)
+			{
+				pstate.forcenonrequired = false;
+				pstate.startikey = 0;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
@@ -1905,6 +1892,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum > pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1914,7 +1902,6 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			if (passes_quals && tuple_alive)
 			{
 				/* tuple passes all scan key conditions */
-				pstate.firstmatch = true;
 				if (!BTreeTupleIsPosting(itup))
 				{
 					/* Remember it */
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 0e6b8b3ab..01beaa4c8 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -57,11 +57,11 @@ static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool forcenonrequired,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-								 ScanDirection dir, bool *continuescan);
+								 ScanDirection dir, bool forcenonrequired, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -1421,9 +1421,10 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
  * postcondition's <= operator with a >=.  In other words, just swap the
  * precondition with the postcondition.)
  *
- * We also deal with "advancing" non-required arrays here.  Callers whose
- * sktrig scan key is non-required specify sktrig_required=false.  These calls
- * are the only exception to the general rule about always advancing the
+ * We also deal with "advancing" non-required arrays here (or arrays that are
+ * treated as non-required for the duration of a _bt_readpage call).  Callers
+ * whose sktrig scan key is non-required specify sktrig_required=false.  These
+ * calls are the only exception to the general rule about always advancing the
  * required array keys (the scan may not even have a required array).  These
  * callers should just pass a NULL pstate (since there is never any question
  * of stopping the scan).  No call to _bt_tuple_before_array_skeys is required
@@ -1463,6 +1464,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 				all_satisfied = true;
 
 	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
+	Assert(_bt_verify_keys_with_arraykeys(scan));
 
 	if (sktrig_required)
 	{
@@ -1472,17 +1474,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 
-		/*
-		 * Required scan key wasn't satisfied, so required arrays will have to
-		 * advance.  Invalidate page-level state that tracks whether the
-		 * scan's required-in-opposite-direction-only keys are known to be
-		 * satisfied by page's remaining tuples.
-		 */
-		pstate->firstmatch = false;
-
-		/* Shouldn't have to invalidate 'prechecked', though */
-		Assert(!pstate->prechecked);
-
 		/*
 		 * Once we return we'll have a new set of required array keys, so
 		 * reset state used by "look ahead" optimization
@@ -1490,8 +1481,26 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		pstate->rechecks = 0;
 		pstate->targetdistance = 0;
 	}
+	else if (sktrig < so->numberOfKeys - 1 &&
+			 !(so->keyData[so->numberOfKeys - 1].sk_flags & SK_SEARCHARRAY))
+	{
+		int			least_sign_ikey = so->numberOfKeys - 1;
+		bool		continuescan;
 
-	Assert(_bt_verify_keys_with_arraykeys(scan));
+		/*
+		 * Optimization: perform a precheck of the least significant key
+		 * during !sktrig_required calls when it isn't already our sktrig
+		 * (provided the precheck key is not itself an array).
+		 *
+		 * When the precheck works out we'll avoid an expensive binary search
+		 * of sktrig's array (plus any other arrays before least_sign_ikey).
+		 */
+		Assert(so->keyData[sktrig].sk_flags & SK_SEARCHARRAY);
+		if (!_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false,
+							   false, &continuescan,
+							   &least_sign_ikey))
+			return false;
+	}
 
 	for (int ikey = 0; ikey < so->numberOfKeys; ikey++)
 	{
@@ -1533,8 +1542,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -1668,7 +1675,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -1710,7 +1717,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -1725,7 +1732,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -1785,6 +1792,12 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * of any required scan key).  All that matters is whether caller's tuple
 	 * satisfies the new qual, so it's safe to just skip the _bt_check_compare
 	 * recheck when we've already determined that it can only return 'false'.
+	 *
+	 * Note: In practice most scan keys are marked required by preprocessing,
+	 * if necessary by generating a preceding skip array.  We nevertheless
+	 * often handle array keys marked required as if they were nonrequired.
+	 * This behavior is requested by our _bt_check_compare caller, though only
+	 * when it is passed "forcenonrequired=true" by _bt_checkkeys.
 	 */
 	if ((sktrig_required && all_required_satisfied) ||
 		(!sktrig_required && all_satisfied))
@@ -1795,9 +1808,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		Assert(all_required_satisfied);
 
 		/* Recheck _bt_check_compare on behalf of caller */
-		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
-							  &continuescan, &nsktrig) &&
+		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false,
+							  false, &continuescan,
+							  &nsktrig) &&
 			!so->scanBehind)
 		{
 			/* This tuple satisfies the new qual */
@@ -2041,8 +2054,9 @@ new_prim_scan:
 	 * read at least one leaf page before the one we're reading now.  This
 	 * makes primscan scheduling more efficient when scanning subsets of an
 	 * index with many distinct attribute values matching many array elements.
-	 * It encourages fewer, larger primitive scans where that makes sense
-	 * (where index descent costs need to be kept under control).
+	 * It encourages fewer, larger primitive scans where that makes sense.
+	 * This will in turn encourage _bt_readpage to apply the pstate.startikey
+	 * optimization more often.
 	 *
 	 * Note: This heuristic isn't as aggressive as you might think.  We're
 	 * conservative about allowing a primitive scan to step from the first
@@ -2199,17 +2213,14 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
  * the page to the right.
  *
  * Advances the scan's array keys when necessary for arrayKeys=true callers.
- * Caller can avoid all array related side-effects when calling just to do a
- * page continuescan precheck -- pass arrayKeys=false for that.  Scans without
- * any arrays keys must always pass arrayKeys=false.
+ * Scans without any array keys must always pass arrayKeys=false.
  *
  * Also stops and starts primitive index scans for arrayKeys=true callers.
  * Scans with array keys are required to set up page state that helps us with
  * this.  The page's finaltup tuple (the page high key for a forward scan, or
  * the page's first non-pivot tuple for a backward scan) must be set in
- * pstate.finaltup ahead of the first call here for the page (or possibly the
- * first call after an initial continuescan-setting page precheck call).  Set
- * this to NULL for rightmost page (or the leftmost page for backwards scans).
+ * pstate.finaltup ahead of the first call here for the page.  Set this to
+ * NULL for rightmost page (or the leftmost page for backwards scans).
  *
  * scan: index scan descriptor (containing a search-type scankey)
  * pstate: page level input and output parameters
@@ -2224,42 +2235,48 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	TupleDesc	tupdesc = RelationGetDescr(scan->indexRelation);
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ScanDirection dir = so->currPos.dir;
-	int			ikey = 0;
+	int			ikey = pstate->startikey;
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
+	Assert(arrayKeys || so->numArrayKeys == 0);
 
-	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
-							&pstate->continuescan, &ikey);
+	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, arrayKeys,
+							pstate->forcenonrequired, &pstate->continuescan,
+							&ikey);
 
+	/*
+	 * If _bt_check_compare relied on the pstate.startikey optimization, call
+	 * again (in assert-enabled builds) to verify it didn't affect our answer.
+	 *
+	 * Note: we can't do this when !pstate.forcenonrequired, since any arrays
+	 * before pstate.startikey won't have advanced on this page at all.
+	 */
+	Assert(!pstate->forcenonrequired || arrayKeys);
 #ifdef USE_ASSERT_CHECKING
-	if (!arrayKeys && so->numArrayKeys)
+	if (pstate->startikey > 0 && !pstate->forcenonrequired)
 	{
-		/*
-		 * This is a continuescan precheck call for a scan with array keys.
-		 *
-		 * Assert that the scan isn't in danger of becoming confused.
-		 */
-		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
-		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
-											 tupnatts, false, 0, NULL));
-	}
-	if (pstate->prechecked || pstate->firstmatch)
-	{
-		bool		dcontinuescan;
+		bool		dres,
+					dcontinuescan;
 		int			dikey = 0;
 
-		/*
-		 * Call relied on continuescan/firstmatch prechecks -- assert that we
-		 * get the same answer without those optimizations
-		 */
-		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
-										&dcontinuescan, &dikey));
+		/* Pass arrayKeys=false to avoid array side-effects */
+		dres = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false,
+								 pstate->forcenonrequired, &dcontinuescan,
+								 &dikey);
+		Assert(res == dres);
 		Assert(pstate->continuescan == dcontinuescan);
+
+		/*
+		 * Should also get the same ikey result.  We need a slightly weaker
+		 * assertion during arrayKeys calls, since they might be using an
+		 * array that couldn't be marked required during preprocessing
+		 * (preprocessing occasionally fails to add a "bridging" skip array,
+		 * due to implementation restrictions around RowCompare keys).
+		 */
+		Assert(arrayKeys || ikey == dikey);
+		Assert(ikey <= dikey);
 	}
 #endif
 
@@ -2280,6 +2297,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * It's also possible that the scan is still _before_ the _start_ of
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
+	Assert(!pstate->forcenonrequired);
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
@@ -2393,8 +2411,9 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 
 	Assert(so->numArrayKeys);
 
-	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc, false,
+					  false, &continuescan,
+					  &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -2402,6 +2421,338 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	return true;
 }
 
+/*
+ * Determines an offset to the first scan key (an so->keyData[]-wise offset)
+ * that is _not_ guaranteed to be satisfied by every tuple from pstate.page,
+ * which is set in pstate.startikey for _bt_checkkeys calls for the page.
+ *
+ * Also determines if later calls to _bt_checkkeys (for pstate.page) should be
+ * forced to treat all required scan keys >= pstate.startikey as nonrequired
+ * (that is, if they're to be treated as if any SK_BT_REQFWD/SK_BT_REQBKWD
+ * markings that were set by preprocessing were not set at all, for the
+ * duration of _bt_checkkeys calls prior to the call for pstate.finaltup).
+ * This is indicated to caller by setting pstate.forcenonrequired.
+ *
+ * Call here at the start of reading a leaf page beyond the first one for the
+ * primitive index scan.  We consider all non-pivot tuples, so it doesn't make
+ * sense to call here when only a subset of those tuples can ever be read.
+ * This is also a good idea on performance grounds; not calling here when on
+ * the first page (first for the current primitive scan) avoids wasting cycles
+ * during selective point queries.  They typically don't stand to gain as much
+ * when we can set pstate.startikey, and are likely to notice the overhead of
+ * calling here.
+ *
+ * Caller must reset pstate.startikey and pstate.forcenonrequired just ahead
+ * of the _bt_checkkeys call for pstate.finaltup tuple.  _bt_checkkeys needs
+ * an opportunity to call _bt_advance_array_keys with sktrig_required=true, to
+ * advance the arrays that will have been ignored when checking prior tuples.
+ * Caller doesn't need to do this on the rightmost/leftmost page in the index
+ * (where pstate.finaltup won't ever be set), though.
+ */
+void
+_bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	ScanDirection dir = so->currPos.dir;
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	ItemId		iid;
+	IndexTuple	firsttup,
+				lasttup;
+	int			startikey = 0,
+				arrayidx = 0,
+				firstchangingattnum;
+	bool		start_past_saop_eq = false;
+
+	/* Should be checked and unset by the time we're called: */
+	Assert(!so->scanBehind);
+
+	/* Only call here when there's at least two non-pivot tuples: */
+	Assert(pstate->minoff < pstate->maxoff);
+	Assert(!pstate->firstpage);
+	Assert(pstate->startikey == 0);
+
+	if (so->numberOfKeys == 0)
+		return;
+
+	/* minoff is an offset to the lowest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->minoff);
+	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* maxoff is an offset to the highest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->maxoff);
+	lasttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* Determine the first attribute whose values change on caller's page */
+	firstchangingattnum = _bt_keep_natts_fast(rel, firsttup, lasttup);
+
+	for (; startikey < so->numberOfKeys; startikey++)
+	{
+		ScanKey		key = so->keyData + startikey;
+		BTArrayKeyInfo *array;
+		Datum		firstdatum,
+					lastdatum;
+		bool		firstnull,
+					lastnull;
+		int32		result;
+
+		/*
+		 * Determine if it's safe to set pstate.startikey to an offset to a
+		 * key that comes after this key, by examining this key
+		 */
+		if (unlikely(!(key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))))
+		{
+			/*
+			 * We only expect to get to a key that's not marked required when
+			 * preprocessing didn't generate a skip array for some prior
+			 * attribute due to its input opclass lacking a usable = operator
+			 * (preprocessing handles RowCompare keys similarly, but we know
+			 * that that can't be the explanation, since we'd have seen the
+			 * RowCompare and set pstate.startikey to its offset already).
+			 *
+			 * This is a rare edge-case, so handle it by giving up completely.
+			 */
+			Assert(!(key->sk_flags & SK_ROW_HEADER));
+			break;				/* assume everything is unsafe (defensive) */
+		}
+		if (key->sk_flags & SK_ROW_HEADER)
+		{
+			/*
+			 * Can't let pstate.startikey get set to an ikey beyond a
+			 * RowCompare inequality
+			 */
+			break;				/* unsafe */
+		}
+		if (key->sk_strategy != BTEqualStrategyNumber)
+		{
+			/*
+			 * Scalar inequality key.
+			 *
+			 * It's definitely safe for _bt_checkkeys to avoid assessing this
+			 * inequality when the page's first and last non-pivot tuples both
+			 * satisfy the inequality (since the same must also be true of all
+			 * the tuples in between these two).
+			 *
+			 * Unlike the "=" case, it doesn't matter if this attribute has
+			 * more than one distinct value (though it _is_ necessary for any
+			 * and all _prior_ attributes to contain no more than one distinct
+			 * value amongst all of the tuples from pstate.page).
+			 */
+			if (key->sk_attno > firstchangingattnum)	/* >, not >= */
+				break;			/* unsafe, preceding attr has multiple
+								 * distinct values */
+
+			firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &firstnull);
+			lastdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &lastnull);
+
+			if (key->sk_flags & SK_ISNULL)
+			{
+				/* IS NOT NULL key */
+				Assert(key->sk_flags & SK_SEARCHNOTNULL);
+
+				if (firstnull || lastnull)
+					break;		/* unsafe */
+
+				/* Safe, IS NOT NULL key satisfied by every tuple */
+				continue;
+			}
+
+			/* Test firsttup */
+			if (firstnull ||
+				!DatumGetBool(FunctionCall2Coll(&key->sk_func,
+												key->sk_collation, firstdatum,
+												key->sk_argument)))
+				break;			/* unsafe */
+
+			/* Test lasttup */
+			if (lastnull ||
+				!DatumGetBool(FunctionCall2Coll(&key->sk_func,
+												key->sk_collation, lastdatum,
+												key->sk_argument)))
+				break;			/* unsafe */
+
+			/* Safe, scalar inequality satisfied by every tuple */
+			continue;
+		}
+
+		/* Some = key (could be a a scalar = key, could be an array = key) */
+		Assert(key->sk_strategy == BTEqualStrategyNumber);
+
+		if (!(key->sk_flags & SK_SEARCHARRAY))
+		{
+			/*
+			 * Scalar = key (posibly an IS NULL key).
+			 *
+			 * It is unsafe to set pstate.startikey to an ikey beyond this
+			 * key, unless the = key is satisfied by every possible tuple on
+			 * the page (possible only when attribute has just one distinct
+			 * value among all tuples on the page).
+			 */
+			if (key->sk_attno >= firstchangingattnum)
+				break;			/* unsafe, multiple distinct attr values */
+
+			firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+									   &firstnull);
+			if (key->sk_flags & SK_ISNULL)
+			{
+				/* IS NULL key */
+				Assert(key->sk_flags & SK_SEARCHNULL);
+
+				if (!firstnull)
+					break;		/* unsafe */
+
+				/* Safe, IS NULL key satisfied by every tuple */
+				continue;
+			}
+			if (firstnull ||
+				!DatumGetBool(FunctionCall2Coll(&key->sk_func,
+												key->sk_collation, firstdatum,
+												key->sk_argument)))
+				break;			/* unsafe */
+
+			/* Safe, scalar = key satisfied by every tuple */
+			continue;
+		}
+
+		/* = array key (could be a SAOP array, could be a skip array) */
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == startikey);
+		if (array->num_elems != -1)
+		{
+			/*
+			 * SAOP array = key.
+			 *
+			 * Handle this like we handle scalar = keys (though binary search
+			 * for a matching element, to avoid relying on key's sk_argument).
+			 */
+			if (key->sk_attno >= firstchangingattnum)
+				break;			/* unsafe, multiple distinct attr values */
+
+			if (startikey < so->numberOfKeys - 1 &&
+				so->keyData[startikey + 1].sk_strategy == BTEqualStrategyNumber &&
+				!so->skipScan)
+			{
+				/*
+				 * SAOP array = key isn't the least significant = key during a
+				 * scan without any skip arrays.
+				 *
+				 * We can safely set startikey to an offset beyond this key
+				 * (in the likely event that the = key is actually satisfied).
+				 * But it would only be safe if we set forcenonrequired=true.
+				 *
+				 * Back out now, so that the scan can use the "look-ahead"
+				 * optimization (and _bt_start_array_keys' cur_elem_trig
+				 * optimization) instead.
+				 */
+				break;
+			}
+
+			firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+									   &firstnull);
+			_bt_binsrch_array_skey(&so->orderProcs[startikey],
+								   false, NoMovementScanDirection,
+								   firstdatum, firstnull, array, key,
+								   &result);
+			if (result != 0)
+				break;			/* unsafe */
+
+			/* Safe, SAOP = key satisfied by every tuple */
+			start_past_saop_eq = true;
+			continue;
+		}
+
+		/*
+		 * Skip array = key.
+		 *
+		 * Handle this like we handle scalar inequality keys (but avoid using
+		 * key's sk_argument/advancing array, as in the SAOP array case).
+		 */
+		if (array->null_elem)
+		{
+			/*
+			 * Safe, non-range skip array "satisfied" by every tuple on page
+			 * (safe even when "key->sk_attno <= firstchangingattnum")
+			 */
+			continue;
+		}
+		else if (key->sk_attno > firstchangingattnum)	/* >, not >= */
+		{
+			break;				/* unsafe, preceding attr has multiple
+								 * distinct values */
+		}
+
+		firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &firstnull);
+		lastdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &lastnull);
+
+		/* Test firsttup */
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   firstdatum, firstnull, array, key,
+								   &result);
+		if (result != 0)
+			break;				/* unsafe */
+
+		/* Test lasttup */
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   lastdatum, lastnull, array, key,
+								   &result);
+		if (result != 0)
+			break;				/* unsafe */
+
+		/* Safe, range skip array satisfied by every tuple */
+	}
+
+	/*
+	 * Use of forcenonrequired is typically undesirable, since it'll force
+	 * _bt_readpage caller to read every tuple on the page -- even though, in
+	 * general, it might well be possible to end the scan on an earlier tuple.
+	 * However, caller must use forcenonrequired when start_past_saop_eq=true,
+	 * since the usual required array behavior might fail to roll over to the
+	 * SAOP array.
+	 *
+	 * We always use forcenonrequired during scans with skip arrays (except on
+	 * the first page of each primitive index scan), though.  While it'd be
+	 * possible to extend the start_past_saop_eq behavior to skip arrays, we
+	 * prefer to always use forcenonrequired -- even when "startikey == 0".
+	 * That way _bt_advance_array_keys's low-order key precheck optimization
+	 * can always be used (unless on the first page of the scan).  In general
+	 * it's worth checking more tuples if that allows us to do significantly
+	 * less skip array maintenance.
+	 */
+	pstate->forcenonrequired = (start_past_saop_eq || so->skipScan);
+	pstate->startikey = startikey;
+
+	if (!pstate->forcenonrequired)
+		return;
+
+	Assert(so->numArrayKeys);
+
+	/*
+	 * Set the element for arrays whose ikey is >= startikey to the lowest
+	 * element value (set it to the highest value when scanning backwards).
+	 * But don't modify the current array element for any earlier arrays.
+	 *
+	 * This allows skip arrays that will never be satisfied by any tuple on
+	 * the page to avoid extra sk_argument comparisons.  _bt_check_compare
+	 * won't use the key's sk_argument when the key is marked MINVAL/MAXVAL.
+	 * Moreover, it's unlikely that MINVAL/MAXVAL will be set for one of these
+	 * arrays unless _bt_advance_array_keys can find an exact match element
+	 * (nonrequired arrays won't "advance" unless an exact match element is
+	 * found, though even that is unlikely due to _bt_advance_array_keys's
+	 * precheck forcing an early "return false", before examining any array).
+	 */
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		key = &so->keyData[array->scan_key];
+
+		if (array->scan_key < startikey)
+			continue;
+
+		_bt_array_set_low_or_high(rel, key, array,
+								  ScanDirectionIsForward(dir));
+	}
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
@@ -2431,23 +2782,33 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  * by the current array key, or if they're truly unsatisfied (that is, if
  * they're unsatisfied by every possible array key).
  *
- * Though we advance non-required array keys on our own, that shouldn't have
- * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
- *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass forcenonrequired=true to instruct us to treat all keys as nonrequired.
+ * This is used to make it safe to temporarily stop properly maintaining the
+ * scan's required arrays.  _bt_checkkeys caller (_bt_readpage, actually)
+ * determines a prefix of keys that must satisfy every possible corresponding
+ * index attribute value from its page, which is passed to us via *ikey arg
+ * (this is the first key that might be unsatisfied by tuples on the page).
+ * Obviously, we won't maintain any array keys from before *ikey, so it's
+ * quite possible for such arrays to "fall behind" the index's keyspace.
+ * Caller will need to "catch up" by passing forcenonrequired=true (alongside
+ * an *ikey=0) once the page's finaltup is reached.
+ *
+ * Note: it's safe to pass an *ikey > 0 with forcenonrequired=false, but only
+ * when caller determines that it won't affect array maintenance.
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool forcenonrequired,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
+	Assert(!forcenonrequired || advancenonrequired);
+
 	*continuescan = true;		/* default assumption */
 
 	for (; *ikey < so->numberOfKeys; (*ikey)++)
@@ -2460,36 +2821,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when we're forced to treat all scan keys as nonrequired)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (forcenonrequired)
+		{
+			/* treating scan's keys as non-required */
+		}
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
 			requiredOppositeDirOnly = true;
 
-		/*
-		 * If the caller told us the *continuescan flag is known to be true
-		 * for the last item on the page, then we know the keys required for
-		 * the current direction scan should be matched.  Otherwise, the
-		 * *continuescan flag would be set for the current item and
-		 * subsequently the last item on the page accordingly.
-		 *
-		 * If the key is required for the opposite direction scan, we can skip
-		 * the check if the caller tells us there was already at least one
-		 * matching item on the page. Also, we require the *continuescan flag
-		 * to be true for the last item on the page to know there are no
-		 * NULLs.
-		 *
-		 * Both cases above work except for the row keys, where NULLs could be
-		 * found in the middle of matching values.
-		 */
-		if (prechecked &&
-			(requiredSameDir || (requiredOppositeDirOnly && firstmatch)) &&
-			!(key->sk_flags & SK_ROW_HEADER))
-			continue;
-
 		if (key->sk_attno > tupnatts)
 		{
 			/*
@@ -2511,6 +2856,19 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		{
 			Assert(key->sk_flags & SK_SEARCHARRAY);
 			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir || forcenonrequired);
+
+			/*
+			 * Cannot fall back on _bt_tuple_before_array_skeys when we're
+			 * treating the scan's keys as nonrequired, though.  Just handle
+			 * this like any other non-required equality-type array key.
+			 */
+			if (forcenonrequired)
+			{
+				Assert(!(key->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)));
+				return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
+											  tupdesc, *ikey, false);
+			}
 
 			*continuescan = false;
 			return false;
@@ -2520,7 +2878,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
-									 continuescan))
+									 forcenonrequired, continuescan))
 				continue;
 			return false;
 		}
@@ -2553,9 +2911,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			 */
 			if (requiredSameDir)
 				*continuescan = false;
+			else if (unlikely(key->sk_flags & SK_BT_SKIP))
+			{
+				/*
+				 * If we're treating scan keys as nonrequired, and encounter a
+				 * skip array scan key whose current element is NULL, then it
+				 * must be a non-range skip array.  It must be satisfied, so
+				 * there's no need to call _bt_advance_array_keys to check.
+				 */
+				Assert(forcenonrequired && *ikey > 0);
+				continue;
+			}
 
 			/*
-			 * In any case, this indextuple doesn't match the qual.
+			 * This indextuple doesn't match the qual.
 			 */
 			return false;
 		}
@@ -2576,7 +2945,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -2594,7 +2963,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -2605,15 +2974,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			return false;
 		}
 
-		/*
-		 * Apply the key-checking function, though only if we must.
-		 *
-		 * When a key is required in the opposite-of-scan direction _only_,
-		 * then it must already be satisfied if firstmatch=true indicates that
-		 * an earlier tuple from this same page satisfied it earlier on.
-		 */
-		if (!(requiredOppositeDirOnly && firstmatch) &&
-			!DatumGetBool(FunctionCall2Coll(&key->sk_func, key->sk_collation,
+		if (!DatumGetBool(FunctionCall2Coll(&key->sk_func, key->sk_collation,
 											datum, key->sk_argument)))
 		{
 			/*
@@ -2663,7 +3024,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
  */
 static bool
 _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
-					 TupleDesc tupdesc, ScanDirection dir, bool *continuescan)
+					 TupleDesc tupdesc, ScanDirection dir,
+					 bool forcenonrequired, bool *continuescan)
 {
 	ScanKey		subkey = (ScanKey) DatumGetPointer(skey->sk_argument);
 	int32		cmpresult = 0;
@@ -2703,7 +3065,11 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 
 		if (isNull)
 		{
-			if (subkey->sk_flags & SK_BT_NULLS_FIRST)
+			if (forcenonrequired)
+			{
+				/* treating scan's keys as non-required */
+			}
+			else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -2757,8 +3123,12 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			 */
 			Assert(subkey != (ScanKey) DatumGetPointer(skey->sk_argument));
 			subkey--;
-			if ((subkey->sk_flags & SK_BT_REQFWD) &&
-				ScanDirectionIsForward(dir))
+			if (forcenonrequired)
+			{
+				/* treating scan's keys as non-required */
+			}
+			else if ((subkey->sk_flags & SK_BT_REQFWD) &&
+					 ScanDirectionIsForward(dir))
 				*continuescan = false;
 			else if ((subkey->sk_flags & SK_BT_REQBKWD) &&
 					 ScanDirectionIsBackward(dir))
@@ -2810,7 +3180,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			break;
 	}
 
-	if (!result)
+	if (!result && !forcenonrequired)
 	{
 		/*
 		 * Tuple fails this qual.  If it's a required qual for the current
@@ -2854,6 +3224,8 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	Assert(!pstate->forcenonrequired);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
-- 
2.49.0

v32-0001-Add-nbtree-skip-scan-optimizations.patchapplication/x-patch; name=v32-0001-Add-nbtree-skip-scan-optimizations.patchDownload
From 1e53645e3edaf358f495d3f2a06ad108259cae32 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v32 1/5] Add nbtree skip scan optimizations.

Teach nbtree composite index scans to opportunistically skip over
irrelevant sections of composite indexes given a query with an omitted
prefix column.  When nbtree is passed input scan keys derived from a
query predicate "WHERE b = 5", new nbtree preprocessing steps now output
"WHERE a = ANY(<every possible 'a' value>) AND b = 5" scan keys.  That
is, preprocessing generates a "skip array" (along with an associated
scan key) for the omitted column "a", which makes it safe to mark the
scan key on "b" as required to continue the scan.  This is far more
efficient than a traditional full index scan whenever it allows the scan
to skip over many irrelevant leaf pages, by iteratively repositioning
itself using the keys on "a" and "b" together.

A skip array has "elements" that are generated procedurally and on
demand, but otherwise works just like a regular ScalarArrayOp array.
Preprocessing can freely add a skip array before or after any input
ScalarArrayOp arrays.  Index scans with a skip array decide when and
where to reposition the scan using the same approach as any other scan
with array keys.  This design builds on the design for array advancement
and primitive scan scheduling added to Postgres 17 by commit 5bf748b8.

The core B-Tree operator classes on most discrete types generate their
array elements with the help of their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the next
possible indexable value frequently turns out to be the next value
stored in the index.  Opclasses that lack a skip support routine fall
back on having nbtree "increment" (or "decrement") a skip array's
current element by setting the NEXT (or PRIOR) scan key flag, without
directly changing the scan key's sk_argument.  These sentinel values
behave just like any other value from an array -- though they can never
locate equal index tuples (they can only locate the next group of index
tuples containing the next set of non-sentinel values that the scan's
arrays need to advance to).

Inequality scan keys can affect how skip arrays generate their values.
Their range is constrained by the inequalities.  For example, a skip
array on "a" will only use element values 1 and 2 given a qual such as
"WHERE a BETWEEN 1 AND 2 AND b = 66".  A scan using such a skip array
has almost identical performance characteristics to one with the qual
"WHERE a = ANY('{1, 2}') AND b = 66".  The scan will be much faster when
it can be executed as two selective primitive index scans instead of a
single very large index scan that reads many irrelevant leaf pages.
However, the array transformation process won't always lead to improved
performance at runtime.  Much depends on physical index characteristics.

B-Tree preprocessing is optimistic about skipping working out: it
applies static, generic rules when determining where to generate skip
arrays, which assumes that the runtime overhead of maintaining skip
arrays will pay for itself -- or lead to only a modest performance loss.
As things stand, these assumptions are much too optimistic: skip array
maintenance will lead to unacceptable regressions with unsympathetic
queries (queries whose scan has little to no chance of avoid reading
irrelevant leaf pages).  An upcoming commit will address the problems in
this area by enhancing _bt_readpage's approach to saving cycles on scan
key evaluation, making it work in a way that directly considers the
needs of = array keys (particularly = skip array keys).

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Reviewed-By: Alena Rybakina <a.rybakina@postgrespro.ru>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |   3 +-
 src/include/access/nbtree.h                   |  34 +-
 src/include/catalog/pg_amproc.dat             |  22 +
 src/include/catalog/pg_proc.dat               |  27 +
 src/include/utils/skipsupport.h               |  98 +++
 src/backend/access/index/indexam.c            |   3 +-
 src/backend/access/nbtree/nbtcompare.c        | 273 +++++++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 621 ++++++++++++--
 src/backend/access/nbtree/nbtree.c            | 195 ++++-
 src/backend/access/nbtree/nbtsearch.c         | 130 ++-
 src/backend/access/nbtree/nbtutils.c          | 755 ++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |   4 +
 src/backend/commands/opclasscmds.c            |  25 +
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/date.c                  |  46 ++
 src/backend/utils/adt/meson.build             |   1 +
 src/backend/utils/adt/selfuncs.c              | 491 +++++++++---
 src/backend/utils/adt/skipsupport.c           |  61 ++
 src/backend/utils/adt/timestamp.c             |  48 ++
 src/backend/utils/adt/uuid.c                  |  70 ++
 doc/src/sgml/btree.sgml                       |  34 +-
 doc/src/sgml/indexam.sgml                     |   3 +-
 doc/src/sgml/indices.sgml                     |  49 +-
 doc/src/sgml/monitoring.sgml                  |   4 +-
 doc/src/sgml/perform.sgml                     |  31 +
 doc/src/sgml/xindex.sgml                      |  16 +-
 src/test/regress/expected/alter_generic.out   |  10 +-
 src/test/regress/expected/btree_index.out     |  41 +
 src/test/regress/expected/create_index.out    | 183 ++++-
 src/test/regress/expected/psql.out            |   3 +-
 src/test/regress/sql/alter_generic.sql        |   5 +-
 src/test/regress/sql/btree_index.sql          |  21 +
 src/test/regress/sql/create_index.sql         |  63 +-
 src/tools/pgindent/typedefs.list              |   3 +
 34 files changed, 2990 insertions(+), 384 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c4a073773..52916bab7 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -214,7 +214,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index faabcb78e..b86bf7bf3 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -707,6 +708,10 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
  *	(BTOPTIONS_PROC).  These procedures define a set of user-visible
  *	parameters that can be used to control operator class behavior.  None of
  *	the built-in B-Tree operator classes currently register an "options" proc.
+ *
+ *	To facilitate more efficient B-Tree skip scans, an operator class may
+ *	choose to offer a sixth amproc procedure (BTSKIPSUPPORT_PROC).  For full
+ *	details, see src/include/utils/skipsupport.h.
  */
 
 #define BTORDER_PROC		1
@@ -714,7 +719,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1027,10 +1033,21 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
-	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for ScalarArrayOpExpr arrays */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+	int			cur_elem;		/* index of current element in elem_values */
+
+	/* fields for skip arrays, which generate element datums procedurally */
+	int16		attlen;			/* attr's length, in bytes */
+	bool		attbyval;		/* attr's FormData_pg_attribute.attbyval */
+	bool		null_elem;		/* NULL is lowest/highest element? */
+	SkipSupport sksup;			/* skip support (NULL if opclass lacks it) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1119,6 +1136,15 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on column without input = */
+
+/* SK_BT_SKIP-only flags (set and unset by array advancement) */
+#define SK_BT_MINVAL	0x00080000	/* invalid sk_argument, use low_compare */
+#define SK_BT_MAXVAL	0x00100000	/* invalid sk_argument, use high_compare */
+#define SK_BT_NEXT		0x00200000	/* positions the scan > sk_argument */
+#define SK_BT_PRIOR		0x00400000	/* positions the scan < sk_argument */
+
+/* Remaps pg_index flag bits to uppermost SK_BT_* byte */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1165,7 +1191,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 19100482b..37000639f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 8b68b16d7..8721f6e11 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2288,6 +2303,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4485,6 +4503,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6364,6 +6385,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9419,6 +9443,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..bc51847cf
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining groups of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * It usually isn't feasible to implement skip support for an opclass whose
+ * input type is continuous.  The B-Tree code falls back on next-key sentinel
+ * values for any opclass that doesn't provide its own skip support function.
+ * This isn't really an implementation restriction; there is no benefit to
+ * providing skip support for an opclass where guessing that the next indexed
+ * value is the next possible indexable value never (or hardly ever) works out.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order)
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 * Operator class decrement/increment functions will never be called with
+	 * a NULL "existing" argument, either.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern SkipSupport PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+												 bool reverse);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 55ec4c103..219df1971 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -489,7 +489,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	if (parallel_aware &&
 		indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 291cb8fc1..4da5a3c1d 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 38a87af1c..75b9e7365 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -45,8 +45,15 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
+static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
+								 ScanKey skey, FmgrInfo *orderproc,
+								 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
+								 BTArrayKeyInfo *array, bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
+							   int *numSkipArrayKeys);
 static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
 									  Oid elemtype, StrategyNumber strat,
 									  Datum *elems, int nelems);
@@ -89,6 +96,8 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -101,10 +110,16 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never generated skip array scan keys, it would be possible for "gaps"
+ * to appear that make it unsafe to mark any subsequent input scan keys
+ * (copied from scan->keyData[]) as required to continue the scan.  Prior to
+ * Postgres 18, a qual like "WHERE y = 4" always resulted in a full scan.
+ * This qual now becomes "WHERE x = ANY('{every possible x value}') and y = 4"
+ * on output.  In other words, preprocessing now adds a skip array on "x".
+ * This has the potential to be much more efficient than a full index scan
+ * (though it behaves like a full scan when there's many distinct "x" values).
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -137,11 +152,21 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * Again, missing cross-type operators might cause us to fail to prove the
  * quals contradictory when they really are, but the scan will work correctly.
  *
- * Row comparison keys are currently also treated without any smarts:
+ * Skip array = keys will even be generated in the presence of "contradictory"
+ * inequality quals when it'll enable marking later input quals as required.
+ * We'll merge any such inequalities into the generated skip array by setting
+ * its array.low_compare or array.high_compare key field.  The resulting skip
+ * array will generate its array elements from a range that's constrained by
+ * any merged input inequalities (which won't get output in so->keyData[]).
+ *
+ * Row comparison keys currently have a couple of notable limitations:
  * we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Also, we are unable to merge a row comparison key
+ * into a skip array (only ordinary inequalities are merged).  A key that
+ * comes after a Row comparison key is therefore never marked as required
+ * (we won't add a useless skip array that can't be merged with a RowCompare).
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -200,6 +225,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProcs[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -229,6 +262,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			Assert(so->keyData[0].sk_flags & SK_SEARCHARRAY);
 			Assert(so->keyData[0].sk_strategy != BTEqualStrategyNumber ||
 				   (so->arrayKeys[0].scan_key == 0 &&
+					!(so->keyData[0].sk_flags & SK_BT_SKIP) &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
 
@@ -288,7 +322,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with regular SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -345,7 +380,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -393,6 +427,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, _bt_preprocess_array_keys has already added "=" keys
+			 * sufficient to form an unbroken series of "=" constraints on all
+			 * attrs prior to the attr from the final scan->keyData[] key.
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -481,6 +520,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -489,6 +529,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -508,8 +549,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -803,6 +842,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -812,6 +854,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -885,6 +943,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -978,24 +1037,55 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
  * Compare an array scan key to a scalar scan key, eliminating contradictory
  * array elements such that the scalar scan key becomes redundant.
  *
+ * If the opfamily is incomplete we may not be able to determine which
+ * elements are contradictory.  When we return true we'll have validly set
+ * *qual_ok, guaranteeing that at least the scalar scan key can be considered
+ * redundant.  We return false if the comparison could not be made (caller
+ * must keep both scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar scan keys.
+ */
+static bool
+_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
+							   bool *qual_ok)
+{
+	Assert(arraysk->sk_attno == skey->sk_attno);
+	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
+		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
+	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
+		   skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Just call the appropriate helper function based on whether it's a SAOP
+	 * array or a skip array.  Both helpers will set *qual_ok in passing.
+	 */
+	if (array->num_elems != -1)
+		return _bt_saoparray_shrink(scan, arraysk, skey, orderproc, array,
+									qual_ok);
+	else
+		return _bt_skiparray_shrink(scan, skey, array, qual_ok);
+}
+
+/*
+ * Preprocessing of SAOP (non-skip) array scan key, used to determine which
+ * array elements are eliminated as contradictory by a non-array scalar key.
+ * _bt_compare_array_scankey_args helper function.
+ *
  * Array elements can be eliminated as contradictory when excluded by some
  * other operator on the same attribute.  For example, with an index scan qual
  * "WHERE a IN (1, 2, 3) AND a < 2", all array elements except the value "1"
  * are eliminated, and the < scan key is eliminated as redundant.  Cases where
  * every array element is eliminated by a redundant scalar scan key have an
  * unsatisfiable qual, which we handle by setting *qual_ok=false for caller.
- *
- * If the opfamily doesn't supply a complete set of cross-type ORDER procs we
- * may not be able to determine which elements are contradictory.  If we have
- * the required ORDER proc then we return true (and validly set *qual_ok),
- * guaranteeing that at least the scalar scan key can be considered redundant.
- * We return false if the comparison could not be made (caller must keep both
- * scan keys when this happens).
  */
 static bool
-_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
-							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
-							   bool *qual_ok)
+_bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+					 FmgrInfo *orderproc, BTArrayKeyInfo *array, bool *qual_ok)
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
@@ -1006,14 +1096,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
-	Assert(arraysk->sk_attno == skey->sk_attno);
 	Assert(array->num_elems > 0);
-	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
-		   arraysk->sk_strategy == BTEqualStrategyNumber);
-	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
-		   skey->sk_strategy != BTEqualStrategyNumber);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
 
 	/*
 	 * _bt_binsrch_array_skey searches an array for the entry best matching a
@@ -1112,6 +1196,105 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	return true;
 }
 
+/*
+ * Preprocessing of skip (non-SAOP) array scan key, used to determine
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function.
+ *
+ * Unlike _bt_saoparray_shrink, we don't modify caller's array in-place.  Skip
+ * arrays work by procedurally generating their elements as needed, so we just
+ * store the inequality as the skip array's low_compare or high_compare.  The
+ * array's elements will be generated from the range of values that satisfies
+ * both low_compare and high_compare.
+ */
+static bool
+_bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
+					 bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Array's index attribute will be constrained by a strict operator/key.
+	 * Array must not "contain a NULL element" (i.e. the scan must not apply
+	 * "IS NULL" qual when it reaches the end of the index that stores NULLs).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Consider if we should treat caller's scalar scan key as the skip
+	 * array's high_compare or low_compare.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way the scan can always safely assume that
+	 * it's okay to use the input-opclass-only-type proc from so->orderProcs[]
+	 * (they can be cross-type with SAOP arrays, but never with skip arrays).
+	 *
+	 * This approach is enabled by MINVAL/MAXVAL sentinel key markings, which
+	 * can be thought of as representing either the lowest or highest matching
+	 * array element (excluding the NULL element, where applicable, though as
+	 * just discussed it isn't applicable to this range skip array anyway).
+	 * Array keys marked MINVAL/MAXVAL never have a valid datum in their
+	 * sk_argument field.  The scan directly applies the array's low_compare
+	 * key when it encounters MINVAL in the array key proper (just as it
+	 * applies high_compare when it sees MAXVAL set in the array key proper).
+	 * The scan must never use the array's so->orderProcs[] proc against
+	 * low_compare's/high_compare's sk_argument, either (so->orderProcs[] is
+	 * only intended to be used with rhs datums from the array proper/index).
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* replace existing high_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing high_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's high_compare */
+			array->high_compare = skey;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* replace existing low_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing low_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's low_compare */
+			array->low_compare = skey;
+			break;
+		case BTEqualStrategyNumber:
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1137,6 +1320,12 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -1144,49 +1333,45 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * references into references to the scan's so->keyData[] output scan keys.
  *
  * Note: the reason we need to return a temp scan key array, rather than just
- * scribbling on scan->keyData, is that callers are permitted to call btrescan
- * without supplying a new set of scankey data.
+ * modifying scan->keyData[], is that callers are permitted to call btrescan
+ * without supplying a new set of scankey data.  Certain other preprocessing
+ * routines (e.g., _bt_fix_scankey_strategy) _can_ modify scan->keyData[], but
+ * we can't make that work here because our modifications are non-idempotent.
  */
 static ScanKey
 _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	Oid			skip_eq_ops[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
-	ScanKey		cur;
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys we'll need to generate below
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -1201,18 +1386,20 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
+		ScanKey		inkey = scan->keyData + input_ikey,
+					cur;
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
 		Oid			elemtype;
@@ -1225,21 +1412,113 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		Datum	   *elem_values;
 		bool	   *elem_nulls;
 		int			num_nonnulls;
-		int			j;
+
+		/* set up next output scan key */
+		cur = &arrayKeyData[numArrayKeyData];
+
+		/* Backfill skip arrays for attrs < or <= input key's attr? */
+		while (numSkipArrayKeys && attno_skip <= inkey->sk_attno)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skip_eq_ops[attno_skip - 1];
+			CompactAttribute *attr;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/*
+				 * Attribute already has an = input key, so don't output a
+				 * skip array for attno_skip.  Just copy attribute's = input
+				 * key into arrayKeyData[] once outside this inner loop.
+				 *
+				 * Note: When we get here there must be a later attribute that
+				 * lacks an equality input key, and still needs a skip array
+				 * (if there wasn't then numSkipArrayKeys would be 0 by now).
+				 */
+				Assert(attno_skip == inkey->sk_attno);
+				/* inkey can't be last input key to be marked required: */
+				Assert(input_ikey < scan->numberOfKeys - 1);
+#if 0
+				/* Could be a redundant input scan key, so can't do this: */
+				Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+					   (inkey->sk_flags & SK_SEARCHNULL));
+#endif
+
+				attno_skip++;
+				break;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize generic BTArrayKeyInfo fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+
+			/* Initialize skip array specific BTArrayKeyInfo fields */
+			attr = TupleDescCompactAttr(RelationGetDescr(rel), attno_skip - 1);
+			reverse = (indoption[attno_skip - 1] & INDOPTION_DESC) != 0;
+			so->arrayKeys[numArrayKeys].attlen = attr->attlen;
+			so->arrayKeys[numArrayKeys].attbyval = attr->attbyval;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup =
+				PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse);
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * We'll need a 3-way ORDER proc.  Set that up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			numArrayKeys++;
+			numArrayKeyData++;	/* keep this scan key/array */
+
+			/* set up next output scan key */
+			cur = &arrayKeyData[numArrayKeyData];
+
+			/* remember having output this skip array and scan key */
+			numSkipArrayKeys--;
+			attno_skip++;
+		}
 
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
-		*cur = scan->keyData[input_ikey];
+		*cur = *inkey;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process conventional/SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -1257,7 +1536,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * all btree operators are strict.
 		 */
 		num_nonnulls = 0;
-		for (j = 0; j < num_elems; j++)
+		for (int j = 0; j < num_elems; j++)
 		{
 			if (!elem_nulls[j])
 				elem_values[num_nonnulls++] = elem_values[j];
@@ -1295,7 +1574,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -1306,7 +1585,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -1323,7 +1602,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -1392,23 +1671,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			origelemtype = elemtype;
 		}
 
-		/*
-		 * And set up the BTArrayKeyInfo data.
-		 *
-		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
-		 * scan_key field later on, after so->keyData[] has been finalized.
-		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		/* Initialize generic BTArrayKeyInfo fields */
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
+
+		/* Initialize SAOP array specific BTArrayKeyInfo fields */
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].cur_elem = -1;	/* i.e. invalid */
+
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
+	Assert(numSkipArrayKeys == 0);
+
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -1514,7 +1794,8 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
 
 			if (array->scan_key == input_ikey)
 			{
@@ -1575,6 +1856,198 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit every so->arrayKeys[] entry.
+ *
+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller must add this many
+ * skip arrays to each of the most significant attributes lacking any keys
+ * that use the = strategy (IS NULL keys count as = keys here).  The specific
+ * attributes that need skip arrays are indicated by initializing caller's
+ * skip_eq_ops[] 0-based attribute offset to a valid = op strategy Oid.  We'll
+ * only ever set skip_eq_ops[] entries to InvalidOid for attributes that
+ * already have an equality key in scan->keyData[] input keys -- and only when
+ * there's some later "attribute gap" for us to "fill-in" with a skip array.
+ *
+ * We're optimistic about skipping working out: we always add exactly the skip
+ * arrays needed to maximize the number of input scan keys that can ultimately
+ * be marked as required to continue the scan (but no more).  For a composite
+ * index on (a, b, c, d), we'll instruct caller to add skip arrays as follows:
+ *
+ * Input keys						Output keys (after all preprocessing)
+ * ----------						-------------------------------------
+ * a = 1							a = 1 (no skip arrays)
+ * a = ANY('{0, 1}')				a = ANY('{0, 1}') (no skip arrays)
+ * b = 42							skip a AND b = 42
+ * b = ANY('{40, 42}')				skip a AND b = ANY('{40, 42}')
+ * a = 1 AND b = 42					a = 1 AND b = 42 (no skip arrays)
+ * a >= 1 AND b = 42				range skip a AND b = 42
+ * a = 1 AND b > 42					a = 1 AND b > 42 (no skip arrays)
+ * a >= 1 AND a <= 3 AND b = 42		range skip a AND b = 42
+ * a = 1 AND c <= 27				a = 1 AND skip b AND c <= 27
+ * a = 1 AND d >= 1					a = 1 AND skip b AND skip c AND d >= 1
+ * a = 1 AND b >= 42 AND d > 1		a = 1 AND range skip b AND skip c AND d > 1
+ */
+static int
+_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_skip = 1,
+				attno_inkey = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numArrayKeys;
+	int			prev_numSkipArrayKeys = 0;
+
+	Assert(scan->numberOfKeys);
+
+	/*
+	 * Initial pass over input scan keys counts the number of conventional
+	 * (non-skip) arrays
+	 */
+	numArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numArrayKeys++;
+	}
+
+#ifdef DEBUG_DISABLE_SKIP_SCAN
+	/* don't attempt to add skip arrays */
+	return numArrayKeys;
+#endif
+
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+			/* Look up input opclass's equality operator (might fail) */
+			skip_eq_ops[attno_skip - 1] =
+				get_opfamily_member(opfamily, opcintype, opcintype,
+									BTEqualStrategyNumber);
+			if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip array for the last input scan key's attribute -- even when
+		 * there are only inequality keys on that attribute.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Later preprocessing steps cannot merge a RowCompare into a skip
+		 * array, so stop adding skip arrays once we see one.  (Note that we
+		 * _can_ backfill skip arrays before a RowCompare, which'll allow keys
+		 * up to and including the RowCompare to be marked required.)
+		 *
+		 * Skip arrays work by maintaining a current array element value,
+		 * which anchors lower-order keys via an implied equality constraint.
+		 * It's not clear how that could ever work with RowCompare quals.
+		 *
+		 * A RowCompare qual "(a, b, c) > (10, 'foo', 42)" is equivalent to
+		 * "((a=10 AND b='foo' AND c>42) OR (a=10 AND b>'foo') OR (a>10))".
+		 * It's possible in principle to anchor some additional lower-order
+		 * key "d" (e.g., "(... (a>10)) AND d = 72"), enabling skipping over
+		 * irrelevant tuples (tuples where "d != 72") within each of the
+		 * resulting disjuncts/scans.  That approach necessitates inventing
+		 * new infrastructure that explicitly rewrites the RowCompare into
+		 * such a series of accesses.  Live without it for now.
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys
+			 */
+			if (attno_has_equal)
+			{
+				/* Attributes with an = key must have InvalidOid eq_op set */
+				skip_eq_ops[attno_skip - 1] = InvalidOid;
+			}
+			else
+			{
+				Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+				Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+				/* Look up input opclass's equality operator (might fail) */
+				skip_eq_ops[attno_skip - 1] =
+					get_opfamily_member(opfamily, opcintype, opcintype,
+										BTEqualStrategyNumber);
+
+				if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+				{
+					/*
+					 * Input opclass lacks an equality strategy operator, so
+					 * don't generate a skip array that definitely won't work
+					 */
+					break;
+				}
+
+				/* plan on adding a backfill skip array for this attribute */
+				(*numSkipArrayKeys)++;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys include any equality strategy
+		 * scan keys (IS NULL keys count as equality keys here).  Also track
+		 * if it has any RowCompare keys.
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	/* numArrayKeys returned to caller includes any skip arrays */
+	return numArrayKeys + *numSkipArrayKeys;
+}
+
 /*
  * _bt_find_extreme_element() -- get least or greatest array element
  *
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 80b04d6ca..815cbcfb7 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -31,6 +31,7 @@
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
 #include "storage/read_stream.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -76,14 +77,26 @@ typedef struct BTParallelScanDescData
 
 	/*
 	 * btps_arrElems is used when scans need to schedule another primitive
-	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
+	 * index scan with one or more SAOP arrays.  Holds BTArrayKeyInfo.cur_elem
+	 * offsets for each = scan key associated with a ScalarArrayOp array.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * Additional space (at the end of the struct) is used when scans need to
+	 * schedule another primitive index scan with one or more skip arrays.
+	 * Holds a flattened datum representation for each = scan key associated
+	 * with a skip array.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -541,10 +554,166 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume that every input scan key will be output with
+	 * its own SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/*
+	 * Pessimistically assume that every index attribute will require a skip
+	 * array (and an associated scan key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum <= nkeyatts; attnum++)
+	{
+		CompactAttribute *attr;
+
+		/*
+		 * We make the conservative assumption that every index column will
+		 * also require a skip array.
+		 *
+		 * Every skip array must have space to store its scan key's sk_flags.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		/* Consider space required to store a datum of opclass input type */
+		attr = TupleDescCompactAttr(rel->rd_att, attnum - 1);
+		if (attr->attbyval)
+		{
+			/* This index attribute stores pass-by-value datums */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  true, attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * This index attribute stores pass-by-reference datums.
+		 *
+		 * Assume that serializing this array will use just as much space as a
+		 * pass-by-value datum, in addition to space for the largest possible
+		 * whole index tuple (this is not just a per-datum portion of the
+		 * largest possible tuple because that'd be almost as large anyway).
+		 *
+		 * This is quite conservative, but it's not clear how we could do much
+		 * better.  The executor requires an up-front storage request size
+		 * that reliably covers the scan's high watermark memory usage.  We
+		 * can't be sure of the real high watermark until the scan is over.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BTMaxItemSize);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+
+		if (array->num_elems != -1)
+		{
+			/* Serialize regular (non-skip) array using cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Serialize skip array key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   array->attbyval, array->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore regular (non-skip) array's cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/*
+		 * Restore skip array key, while freeing memory allocated for old
+		 * sk_argument where required
+		 */
+		if (!array->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -613,6 +782,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -679,14 +849,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -831,6 +996,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -849,12 +1015,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
 	LWLockRelease(&btscan->btps_lock);
 }
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 3d46fb5df..1ef2cb2b5 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -983,7 +983,21 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * one we use --- by definition, they are either redundant or
 	 * contradictory.
 	 *
-	 * Any regular (not SK_SEARCHNULL) key implies a NOT NULL qualifier.
+	 * In practice we rarely see any "attribute boundary key gaps" here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  All
+	 * array keys are always considered = keys, but we'll sometimes need to
+	 * treat the current key value as if we were using an inequality strategy.
+	 * This happens with range skip arrays, which store inequality keys in the
+	 * array's low_compare/high_compare fields (used to find the first/last
+	 * set of matches, when = key will lack a usable sk_argument value).
+	 * These are always preferred over any redundant "standard" inequality
+	 * keys on the same column (per the usual rule about preferring = keys).
+	 * Note also that any column with an = skip array key can never have an
+	 * additional, contradictory = key.
+	 *
+	 * All keys (with the exception of SK_SEARCHNULL keys and SK_BT_SKIP
+	 * array keys whose array is "null_elem=true") imply a NOT NULL qualifier.
 	 * If the index stores nulls at the end of the index we'll be starting
 	 * from, and we have no boundary key for the column (which means the key
 	 * we deduced NOT NULL from is an inequality key that constrains the other
@@ -1040,8 +1054,54 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is MINVAL, choose low_compare (when scanning
+				 * backwards it'll be MAXVAL, and we'll choose high_compare).
+				 *
+				 * Note: if the array's low_compare key makes 'chosen' NULL,
+				 * then we behave as if the array's first element is -inf,
+				 * except when !array->null_elem implies a usable NOT NULL
+				 * constraint.
+				 */
+				if (chosen != NULL &&
+					(chosen->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skipequalitykey = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MAXVAL));
+						chosen = array->low_compare;
+					}
+					else
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MINVAL));
+						chosen = array->high_compare;
+					}
+
+					Assert(chosen == NULL ||
+						   chosen->sk_attno == skipequalitykey->sk_attno);
+
+					if (!array->null_elem)
+						impliesNN = skipequalitykey;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1084,9 +1144,40 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 
 				/*
-				 * Done if that was the last attribute, or if next key is not
-				 * in sequence (implying no boundary key is available for the
-				 * next attribute).
+				 * If the key that we just added to startKeys[] is a skip
+				 * array = key whose current element is marked NEXT or PRIOR,
+				 * make strat_total > or < (and stop adding boundary keys).
+				 * This can only happen with opclasses that lack skip support.
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(strat_total == BTEqualStrategyNumber);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We're done.  We'll never find an exact = match for a
+					 * NEXT or PRIOR sentinel sk_argument value.  There's no
+					 * sense in trying to add more keys to startKeys[].
+					 */
+					break;
+				}
+
+				/*
+				 * Done if that was the last scan key output by preprocessing.
+				 * Also done if there is a gap index attribute that lacks a
+				 * usable key (only possible when preprocessing was unable to
+				 * generate a skip array key to "fill in the gap").
 				 */
 				if (i >= so->numberOfKeys ||
 					cur->sk_attno != curattr + 1)
@@ -1581,31 +1672,10 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * We skip this for the first page read by each (primitive) scan, to avoid
 	 * slowing down point queries.  They typically don't stand to gain much
 	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
-	 *
-	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
-	 * just set a low-order required array's key to the best available match
-	 * for a truncated -inf attribute value from the prior page's high key
-	 * (array element 0 is always the best available match in this scenario).
-	 * It's quite likely that matches for array element 0 begin on this page,
-	 * but the start of matches won't necessarily align with page boundaries.
-	 * When the start of matches is somewhere in the middle of this page, it
-	 * would be wrong to treat page's final non-pivot tuple as representative.
-	 * Doing so might lead us to treat some of the page's earlier tuples as
-	 * being part of a group of tuples thought to satisfy the required keys.
-	 *
-	 * Note: Conversely, in the case where the scan's arrays just advanced
-	 * using the prior page's HIKEY _without_ advancement setting scanBehind,
-	 * the start of matches must be aligned with page boundaries, which makes
-	 * it safe to attempt the optimization here now.  It's also safe when the
-	 * prior page's HIKEY simply didn't need to advance any required array. In
-	 * both cases we can safely assume that the _first_ tuple from this page
-	 * must be >= the current set of array keys/equality constraints. And so
-	 * if the final tuple is == those same keys (and also satisfies any
-	 * required < or <= strategy scan keys) during the precheck, we can safely
-	 * assume that this must also be true of all earlier tuples from the page.
+	 * overhead of the precheck.  Also avoid it during scans with array keys,
+	 * which might be using skip scan (XXX fixed in next commit).
 	 */
-	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !arrayKeys && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 2aee9bbf6..0e6b8b3ab 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -30,6 +30,17 @@
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									  int32 set_elem_result, Datum tupdatum, bool tupnull);
+static void _bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_array_set_low_or_high(Relation rel, ScanKey skey,
+									  BTArrayKeyInfo *array, bool low_not_high);
+static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -207,6 +218,7 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 	int32		result = 0;
 
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
+	Assert(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)));
 
 	if (tupnull)				/* NULL tupdatum */
 	{
@@ -283,6 +295,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* SAOP arrays never have NULLs */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -405,6 +419,185 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, since skip arrays don't really
+ * contain elements (they generate their array elements procedurally instead).
+ * Our interface matches that of _bt_binsrch_array_skey in every other way.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  We return -1 when tupdatum/tupnull is lower that any value
+ * within the range of the array, and 1 when it is higher than every value.
+ * Caller should pass *set_elem_result to _bt_skiparray_set_element to advance
+ * the array.
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (array->null_elem)
+	{
+		Assert(!array->low_compare && !array->high_compare);
+
+		*set_elem_result = 0;
+		return;
+	}
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+
+	/*
+	 * Assert that any keys that were assumed to be satisfied already (due to
+	 * caller passing cur_elem_trig=true) really are satisfied as expected
+	 */
+#ifdef USE_ASSERT_CHECKING
+	if (cur_elem_trig)
+	{
+		if (ScanDirectionIsForward(dir) && array->low_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												  array->low_compare->sk_collation,
+												  tupdatum,
+												  array->low_compare->sk_argument)));
+
+		if (ScanDirectionIsBackward(dir) && array->high_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												  array->high_compare->sk_collation,
+												  tupdatum,
+												  array->high_compare->sk_argument)));
+	}
+#endif
+}
+
+/*
+ * _bt_skiparray_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Caller passes set_elem_result returned by _bt_binsrch_skiparray_skey for
+ * caller's tupdatum/tupnull.
+ *
+ * We copy tupdatum/tupnull into skey's sk_argument iff set_elem_result == 0.
+ * Otherwise, we set skey to either the lowest or highest value that's within
+ * the range of caller's skip array (whichever is the best available match to
+ * tupdatum/tupnull that is still within the range of the skip array according
+ * to _bt_binsrch_skiparray_skey/set_elem_result).
+ */
+static void
+_bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  int32 set_elem_result, Datum tupdatum, bool tupnull)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (set_elem_result)
+	{
+		/* tupdatum/tupnull is out of the range of the skip array */
+		Assert(!array->null_elem);
+
+		_bt_array_set_low_or_high(rel, skey, array, set_elem_result < 0);
+		return;
+	}
+
+	/* Advance skip array to tupdatum (or tupnull) value */
+	if (unlikely(tupnull))
+	{
+		_bt_skiparray_set_isnull(rel, skey, array);
+		return;
+	}
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen);
+}
+
+/*
+ * _bt_skiparray_set_isnull() -- set skip array scan key to NULL
+ */
+static void
+_bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->null_elem && !array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Set sk_argument to NULL */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -414,29 +607,355 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_array_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_array_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  bool low_not_high)
+{
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for conventional array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting array to lowest element (according to low_compare) */
+		skey->sk_flags |= SK_BT_MINVAL;
+	}
+	else
+	{
+		/* Setting array to highest element (according to high_compare) */
+		skey->sk_flags |= SK_BT_MAXVAL;
+	}
+}
+
+/*
+ * _bt_array_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the minimum value within the range
+	 * of a skip array (often just -inf) is never decrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		/* Successfully "decremented" array */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly decrement sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Decrement" from NULL to the high_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->high_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup->decrement(rel, skey->sk_argument, &uflow);
+	if (unlikely(uflow))
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			_bt_skiparray_set_isnull(rel, skey, array);
+
+			/* Successfully "decremented" array to NULL */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass decrement callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	/* Successfully decremented array */
+	return true;
+}
+
+/*
+ * _bt_array_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MINVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* Regular (non-skip) array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the maximum value within the range
+	 * of a skip array (often just +inf) is never incrementable
+	 */
+	if (skey->sk_flags & SK_BT_MAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		/* Successfully "incremented" array */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly increment sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Increment" from NULL to the low_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->low_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup->increment(rel, skey->sk_argument, &oflow);
+	if (unlikely(oflow))
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			_bt_skiparray_set_isnull(rel, skey, array);
+
+			/* Successfully "incremented" array to NULL */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept non-NULL datum value from opclass increment callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	/* Successfully incremented array */
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -452,6 +971,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -461,29 +981,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_array_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_array_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -507,7 +1028,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 }
 
 /*
- * _bt_rewind_nonrequired_arrays() -- Rewind non-required arrays
+ * _bt_rewind_nonrequired_arrays() -- Rewind SAOP arrays not marked required
  *
  * Called when _bt_advance_array_keys decides to start a new primitive index
  * scan on the basis of the current scan position being before the position
@@ -539,10 +1060,15 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
  *
  * Note: _bt_verify_arrays_bt_first is called by an assertion to enforce that
  * everybody got this right.
+ *
+ * Note: In practice almost all SAOP arrays are marked required during
+ * preprocessing (if necessary by generating skip arrays).  It is hardly ever
+ * truly necessary to call here, but consistently doing so is simpler.
  */
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -550,7 +1076,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -562,16 +1087,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_array_set_low_or_high(rel, cur, array,
+								  ScanDirectionIsForward(dir));
 	}
 }
 
@@ -696,9 +1215,77 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (likely(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))))
+		{
+			/* Scankey has a valid/comparable sk_argument value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0)
+			{
+				/*
+				 * Interpret result in a way that takes NEXT/PRIOR into
+				 * account
+				 */
+				if (cur->sk_flags & SK_BT_NEXT)
+					result = -1;
+				else if (cur->sk_flags & SK_BT_PRIOR)
+					result = 1;
+
+				Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP));
+			}
+		}
+		else
+		{
+			BTArrayKeyInfo *array = NULL;
+
+			/*
+			 * Current array element/array = scan key value is a sentinel
+			 * value that represents the lowest (or highest) possible value
+			 * that's still within the range of the array.
+			 *
+			 * Like _bt_first, we only see MINVAL keys during forwards scans
+			 * (and similarly only see MAXVAL keys during backwards scans).
+			 * Even if the scan's direction changes, we'll stop at some higher
+			 * order key before we can ever reach any MAXVAL (or MINVAL) keys.
+			 * (However, unlike _bt_first we _can_ get to keys marked either
+			 * NEXT or PRIOR, regardless of the scan's current direction.)
+			 */
+			Assert(ScanDirectionIsForward(dir) ?
+				   !(cur->sk_flags & SK_BT_MAXVAL) :
+				   !(cur->sk_flags & SK_BT_MINVAL));
+
+			/*
+			 * There are no valid sk_argument values in MINVAL/MAXVAL keys.
+			 * Check if tupdatum is within the range of skip array instead.
+			 */
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			_bt_binsrch_skiparray_skey(false, dir, tupdatum, tupnull,
+									   array, cur, &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum satisfies both low_compare and high_compare, so
+				 * it's time to advance the array keys.
+				 *
+				 * Note: It's possible that the skip array will "advance" from
+				 * its MINVAL (or MAXVAL) representation to an alternative,
+				 * logically equivalent representation of the same value: a
+				 * representation where the = key gets a valid datum in its
+				 * sk_argument.  This is only possible when low_compare uses
+				 * the >= strategy (or high_compare uses the <= strategy).
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1017,18 +1604,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1053,18 +1631,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -1080,14 +1649,22 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			bool		cur_elem_trig = (sktrig_required && ikey == sktrig);
 
 			/*
-			 * Binary search for closest match that's available from the array
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems == -1)
+				_bt_binsrch_skiparray_skey(cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * Binary search for the closest match from the SAOP array
+			 */
+			else
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 		}
 		else
 		{
@@ -1163,11 +1740,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+		if (array)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->num_elems == -1)
+			{
+				/* Skip array's new element is tupdatum (or MINVAL/MAXVAL) */
+				_bt_skiparray_set_element(rel, cur, array, result,
+										  tupdatum, tupnull);
+			}
+			else if (array->cur_elem != set_elem)
+			{
+				/* SAOP array's new element is set_elem datum */
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
 		}
 	}
 
@@ -1581,10 +2168,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -1914,6 +2502,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key uses one of several sentinel values.  We just
+		 * fall back on _bt_tuple_before_array_skeys when we see such a value.
+		 */
+		if (key->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL |
+							 SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
@@ -1939,6 +2541,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			else
 			{
 				Assert(key->sk_flags & SK_SEARCHNOTNULL);
+				Assert(!(key->sk_flags & SK_BT_SKIP));
 				if (!isNull)
 					continue;	/* tuple satisfies this qual */
 			}
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index dd6f5a15c..817ad358f 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -106,6 +106,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 8546366ee..a6dd8eab5 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("ordering equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (GetIndexAmRoutineByAmId(amoid, false)->amcanhash)
 	{
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index f279853de..4227ab1a7 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f23cfad71..244f48f4f 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 5b35debc8..385c20c62 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -193,6 +193,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -214,6 +216,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5943,6 +5947,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -7001,6 +7091,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -7010,17 +7147,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
+	List	   *indexSkipQuals;
 	int			indexcol;
 	bool		eqQualHere;
-	bool		found_saop;
+	bool		found_row_compare;
+	bool		found_array;
 	bool		found_is_null_op;
+	bool		have_correlation = false;
 	double		num_sa_scans;
+	double		correlation = 0.0;
 	ListCell   *lc;
 
 	/*
@@ -7031,19 +7170,24 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * it's OK to count them in indexSelectivity, but they should not count
 	 * for estimating numIndexTuples.  So we must examine the given indexquals
 	 * to find out which ones count as boundary quals.  We rely on the
-	 * knowledge that they are given in index column order.
+	 * knowledge that they are given in index column order.  Note that nbtree
+	 * preprocessing can add skip arrays that act as leading '=' quals in the
+	 * absence of ordinary input '=' quals, so in practice _most_ input quals
+	 * are able to act as index bound quals (which we take into account here).
 	 *
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
+	 * If there's a SAOP or skip array in the quals, we'll actually perform up
+	 * to N index descents (not just one), but the underlying array key's
 	 * operator can be considered to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
+	indexSkipQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
-	found_saop = false;
+	found_row_compare = false;
+	found_array = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -7051,17 +7195,203 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
 		ListCell   *lc2;
 
-		if (indexcol != iclause->indexcol)
+		if (indexcol < iclause->indexcol)
 		{
-			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			double		num_sa_scans_prev_cols = num_sa_scans;
+
+			/*
+			 * Beginning of a new column's quals.
+			 *
+			 * Skip scans use skip arrays, which are ScalarArrayOp style
+			 * arrays that generate their elements procedurally and on demand.
+			 * Given a composite index on "(a, b)", and an SQL WHERE clause
+			 * "WHERE b = 42", a skip scan will effectively use an indexqual
+			 * "WHERE a = ANY('{every col a value}') AND b = 42".  (Obviously,
+			 * the array on "a" must also return "IS NULL" matches, since our
+			 * WHERE clause used no strict operator on "a").
+			 *
+			 * Here we consider how nbtree will backfill skip arrays for any
+			 * index columns that lacked an '=' qual.  This maintains our
+			 * num_sa_scans estimate, and determines if this new column (the
+			 * "iclause->indexcol" column, not the prior "indexcol" column)
+			 * can have its RestrictInfos/quals added to indexBoundQuals.
+			 *
+			 * We'll need to handle columns that have inequality quals, where
+			 * the skip array generates values from a range constrained by the
+			 * quals (not every possible value).  We've been maintaining
+			 * indexSkipQuals to help with this; it will now contain all of
+			 * the prior column's quals (that is, indexcol's quals) when they
+			 * might be used for this.
+			 */
+			if (found_row_compare)
+			{
+				/*
+				 * Skip arrays can't be added after a RowCompare input qual
+				 * due to limitations in nbtree
+				 */
+				break;
+			}
+			if (eqQualHere)
+			{
+				/*
+				 * Don't need to add a skip array for an indexcol that already
+				 * has an '=' qual/equality constraint
+				 */
+				indexcol++;
+				indexSkipQuals = NIL;
+			}
 			eqQualHere = false;
-			indexcol++;
+
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct;
+				bool		isdefault = true;
+
+				found_array = true;
+
+				/*
+				 * A skipped attribute's ndistinct forms the basis of our
+				 * estimate of the total number of "array elements" used by
+				 * its skip array at runtime.  Look that up first.
+				 */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+				if (indexcol == 0)
+				{
+					/*
+					 * Get an estimate of the leading column's correlation in
+					 * passing (avoids rereading variable stats below)
+					 */
+					if (HeapTupleIsValid(vardata.statsTuple))
+						correlation = btcost_correlation(index, &vardata);
+					have_correlation = true;
+				}
+
+				ReleaseVariableStats(vardata);
+
+				/*
+				 * If ndistinct is a default estimate, conservatively assume
+				 * that no skipping will happen at runtime
+				 */
+				if (isdefault)
+				{
+					num_sa_scans = num_sa_scans_prev_cols;
+					break;		/* done building indexBoundQuals */
+				}
+
+				/*
+				 * Apply indexcol's indexSkipQuals selectivity to ndistinct
+				 */
+				if (indexSkipQuals != NIL)
+				{
+					List	   *partialSkipQuals;
+					Selectivity ndistinctfrac;
+
+					/*
+					 * If the index is partial, AND the index predicate with
+					 * the index-bound quals to produce a more accurate idea
+					 * of the number of distinct values for prior indexcol
+					 */
+					partialSkipQuals = add_predicate_to_index_quals(index,
+																	indexSkipQuals);
+
+					ndistinctfrac = clauselist_selectivity(root, partialSkipQuals,
+														   index->rel->relid,
+														   JOIN_INNER,
+														   NULL);
+
+					/*
+					 * If ndistinctfrac is selective (on its own), the scan is
+					 * unlikely to benefit from repositioning itself using
+					 * later quals.  Do not allow iclause->indexcol's quals to
+					 * be added to indexBoundQuals (it would increase descent
+					 * costs, without lowering numIndexTuples costs by much).
+					 */
+					if (ndistinctfrac < DEFAULT_RANGE_INEQ_SEL)
+					{
+						num_sa_scans = num_sa_scans_prev_cols;
+						break;	/* done building indexBoundQuals */
+					}
+
+					/* Adjust ndistinct downward */
+					ndistinct = rint(ndistinct * ndistinctfrac);
+					ndistinct = Max(ndistinct, 1);
+				}
+
+				/*
+				 * When there's no inequality quals, account for the need to
+				 * find an initial value by counting -inf/+inf as a value.
+				 *
+				 * We don't charge anything extra for possible next/prior key
+				 * index probes, which are sometimes used to find the next
+				 * valid skip array element (ahead of using the located
+				 * element value to relocate the scan to the next position
+				 * that might contain matching tuples).  It seems hard to do
+				 * better here.  Use of the skip support infrastructure often
+				 * avoids most next/prior key probes.  But even when it can't,
+				 * there's a decent chance that most individual next/prior key
+				 * probes will locate a leaf page whose key space overlaps all
+				 * of the scan's keys (even the lower-order keys) -- which
+				 * also avoids the need for a separate, extra index descent.
+				 * Note also that these probes are much cheaper than non-probe
+				 * primitive index scans: they're reliably very selective.
+				 */
+				if (indexSkipQuals == NIL)
+					ndistinct += 1;
+
+				/*
+				 * Update num_sa_scans estimate by multiplying by ndistinct.
+				 *
+				 * We make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * expecting skipping to be helpful...
+				 */
+				num_sa_scans *= ndistinct;
+
+				/*
+				 * ...but back out of adding this latest group of 1 or more
+				 * skip arrays when num_sa_scans exceeds the total number of
+				 * index pages (revert to num_sa_scans from before indexcol).
+				 * This causes a sharp discontinuity in cost (as a function of
+				 * the indexcol's ndistinct), but that is representative of
+				 * actual runtime costs.
+				 *
+				 * Note that skipping is helpful when each primitive index
+				 * scan only manages to skip over 1 or 2 irrelevant leaf pages
+				 * on average.  Skip arrays bring savings in CPU costs due to
+				 * the scan not needing to evaluate indexquals against every
+				 * tuple, which can greatly exceed any savings in I/O costs.
+				 * This test is a test of whether num_sa_scans implies that
+				 * we're past the point where the ability to skip ceases to
+				 * lower the scan's costs (even qual evaluation CPU costs).
+				 */
+				if (index->pages < num_sa_scans)
+				{
+					num_sa_scans = num_sa_scans_prev_cols;
+					break;		/* done building indexBoundQuals */
+				}
+
+				indexcol++;
+				indexSkipQuals = NIL;
+			}
+
+			/*
+			 * Finished considering the need to add skip arrays to bridge an
+			 * initial eqQualHere gap between the old and new index columns
+			 * (or there was no initial eqQualHere gap in the first place).
+			 *
+			 * If an initial gap could not be bridged, then new column's quals
+			 * (i.e. iclause->indexcol's quals) won't go into indexBoundQuals,
+			 * and so won't affect our final numIndexTuples estimate.
+			 */
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+				break;			/* done building indexBoundQuals */
 		}
 
+		Assert(indexcol == iclause->indexcol);
+
 		/* Examine each indexqual associated with this index clause */
 		foreach(lc2, iclause->indexquals)
 		{
@@ -7081,6 +7411,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_row_compare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -7089,7 +7420,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				double		alength = estimate_array_length(root, other_operand);
 
 				clause_op = saop->opno;
-				found_saop = true;
+				found_array = true;
 				/* estimate SA descents by indexBoundQuals only */
 				if (alength > 1)
 					num_sa_scans *= alength;
@@ -7101,7 +7432,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skip scan purposes */
 					eqQualHere = true;
 				}
 			}
@@ -7120,19 +7451,28 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
+
+			/*
+			 * We apply inequality selectivities to estimate index descent
+			 * costs with scans that use skip arrays.  Save this indexcol's
+			 * RestrictInfos if it looks like they'll be needed for that.
+			 */
+			if (!eqQualHere && !found_row_compare &&
+				indexcol < index->nkeycolumns - 1)
+				indexSkipQuals = lappend(indexSkipQuals, rinfo);
 		}
 	}
 
 	/*
 	 * If index is unique and we found an '=' clause for each column, we can
 	 * just assume numIndexTuples = 1 and skip the expensive
-	 * clauselist_selectivity calculations.  However, a ScalarArrayOp or
-	 * NullTest invalidates that theory, even though it sets eqQualHere.
+	 * clauselist_selectivity calculations.  However, an array or NullTest
+	 * always invalidates that theory (even when eqQualHere has been set).
 	 */
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
-		!found_saop &&
+		!found_array &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
 	else
@@ -7154,7 +7494,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		numIndexTuples = btreeSelectivity * index->rel->tuples;
 
 		/*
-		 * btree automatically combines individual ScalarArrayOpExpr primitive
+		 * btree automatically combines individual array element primitive
 		 * index scans whenever the tuples covered by the next set of array
 		 * keys are close to tuples covered by the current set.  That puts a
 		 * natural ceiling on the worst case number of descents -- there
@@ -7172,16 +7512,18 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * of leaf pages (we make it 1/3 the total number of pages instead) to
 		 * give the btree code credit for its ability to continue on the leaf
 		 * level with low selectivity scans.
+		 *
+		 * Note: num_sa_scans includes both ScalarArrayOp array elements and
+		 * skip array elements whose qual affects our numIndexTuples estimate.
 		 */
 		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
-		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
-		 * to integer.
+		 * As in genericcostestimate(), we have to adjust for any array quals
+		 * included in indexBoundQuals, and then round to integer.
 		 *
-		 * It is tempting to make genericcostestimate behave as if SAOP
+		 * It is tempting to make genericcostestimate behave as if array
 		 * clauses work in almost the same way as scalar operators during
 		 * btree scans, making the top-level scan look like a continuous scan
 		 * (as opposed to num_sa_scans-many primitive index scans).  After
@@ -7214,7 +7556,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * comparisons to descend a btree of N leaf tuples.  We charge one
 	 * cpu_operator_cost per comparison.
 	 *
-	 * If there are ScalarArrayOpExprs, charge this once per estimated SA
+	 * If there are SAOP/skip array keys, charge this once per estimated SA
 	 * index descent.  The ones after the first one are not startup cost so
 	 * far as the overall plan goes, so just add them to "total" cost.
 	 */
@@ -7234,110 +7576,25 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * cost is somewhat arbitrarily set at 50x cpu_operator_cost per page
 	 * touched.  The number of such pages is btree tree height plus one (ie,
 	 * we charge for the leaf page too).  As above, charge once per estimated
-	 * SA index descent.
+	 * SAOP/skip array descent.
 	 */
 	descentCost = (index->tree_height + 1) * DEFAULT_PAGE_CPU_MULTIPLIER * cpu_operator_cost;
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* btcost_correlation already called earlier on */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..2bd35d2d2
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns skip support struct, allocating in caller's memory
+ * context.  Otherwise returns NULL, indicating that operator class has no
+ * skip support function.
+ */
+SkipSupport
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse)
+{
+	Oid			skipSupportFunction;
+	SkipSupport sksup;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return NULL;
+
+	sksup = palloc(sizeof(SkipSupportData));
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return sksup;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 9682f9dbd..347089b76 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2304,6 +2305,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 4f8402ef9..1b72059d2 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,6 +13,7 @@
 
 #include "postgres.h"
 
+#include <limits.h>
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
@@ -21,6 +22,7 @@
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -416,6 +418,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..3e6f30d74 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value that
+     might be stored in an index, so the domain of the particular data type
+     stored within the index (the input opclass type) must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index 768b77aa0..d5adb58c1 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -835,7 +835,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index bacc09cb8..0fdd0cbb1 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4263,7 +4263,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage
      <replaceable>column_name</replaceable> =
      <replaceable>value2</replaceable> ...</literal> construct, though only
     when the optimizer transforms the construct into an equivalent
-    multi-valued array representation.
+    multi-valued array representation.  Similarly, when B-Tree index scans use
+    the skip scan strategy, an index search is performed each time the scan is
+    repositioned to the next index leaf page that might have matching tuples.
    </para>
   </note>
   <tip>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index 387baac7e..d0470ac79 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -860,6 +860,37 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE thousand IN (1, 2, 3, 4);
     <structname>tenk1_thous_tenthous</structname> index leaf page.
    </para>
 
+   <para>
+    The <quote>Index Searches</quote> line is also useful with B-tree index
+    scans that apply the <firstterm>skip scan</firstterm> optimization to
+    more efficiently traverse through an index:
+<screen>
+EXPLAIN ANALYZE SELECT four, unique1 FROM tenk1 WHERE four BETWEEN 1 AND 3 AND unique1 = 42;
+                                                              QUERY PLAN
+-------------------------------------------------------------------&zwsp;---------------------------------------------------------------
+ Index Only Scan using tenk1_four_unique1_idx on tenk1  (cost=0.29..6.90 rows=1 width=8) (actual time=0.006..0.007 rows=1.00 loops=1)
+   Index Cond: ((four &gt;= 1) AND (four &lt;= 3) AND (unique1 = 42))
+   Heap Fetches: 0
+   Index Searches: 3
+   Buffers: shared hit=7
+ Planning Time: 0.029 ms
+ Execution Time: 0.012 ms
+</screen>
+
+    Here we see an Index-Only Scan node using
+    <structname>tenk1_four_unique1_idx</structname>, a composite index on the
+    <structname>tenk1</structname> table's <structfield>four</structfield> and
+    <structfield>unique1</structfield> columns.  The scan performs 3 searches
+    that each read a single index leaf page:
+    <quote><literal>four = 1 AND unique1 = 42</literal></quote>,
+    <quote><literal>four = 2 AND unique1 = 42</literal></quote>, and
+    <quote><literal>four = 3 AND unique1 = 42</literal></quote>.  This index
+    is generally a good target for skip scan, since its leading column (the
+    <structfield>four</structfield> column) contains only 4 distinct values,
+    while its second/final column (the <structfield>unique1</structfield>
+    column) contains many distinct values.
+   </para>
+
    <para>
     Another type of extra information is the number of rows removed by a
     filter condition:
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 053619624..7e23a7b6e 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index 0c274d56a..23bf33f10 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  ordering equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 8879554c3..bfb1a286e 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -581,6 +581,47 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Index Only Scan using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Index Only Scan Backward using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 --
 -- Test for multilevel page deletion
 --
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index bd5f002cf..15dde752f 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1637,7 +1637,9 @@ DROP TABLE syscol_table;
 -- Tests for IS NULL/IS NOT NULL with b-tree indexes
 --
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 SET enable_seqscan = OFF;
 SET enable_indexscan = ON;
@@ -1645,7 +1647,7 @@ SET enable_bitmapscan = ON;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1657,13 +1659,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1678,12 +1680,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1695,13 +1703,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1722,12 +1730,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0,
      1
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc nulls last,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1739,13 +1753,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1760,12 +1774,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2  nulls first,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1777,13 +1797,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1798,6 +1818,12 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 -- Check initial-positioning logic too
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2);
@@ -1829,20 +1855,24 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
 (2 rows)
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-         |        
-     278 |     999
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 5;
+ unique1 |  unique2   
+---------+------------
+     500 |           
+     100 |           
+         |           
+         | 2147483647
+     278 |        999
+(5 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-     278 |     999
-       0 |     998
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 3;
+ unique1 |  unique2   
+---------+------------
+         | 2147483647
+     278 |        999
+       0 |        998
+(3 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
@@ -2247,7 +2277,8 @@ SELECT count(*) FROM dupindexcols
 (1 row)
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 explain (costs off)
 SELECT unique1 FROM tenk1
@@ -2269,7 +2300,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2289,7 +2320,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2309,6 +2340,25 @@ ORDER BY thousand DESC, tenthous DESC;
         0 |     3000
 (2 rows)
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ Index Only Scan Backward using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > 995) AND (tenthous = ANY ('{998,999}'::integer[])))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+ thousand | tenthous 
+----------+----------
+      999 |      999
+      998 |      998
+(2 rows)
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -2339,6 +2389,45 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 ---------
 (0 rows)
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+                   QUERY PLAN                    
+-------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY (NULL::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY ('{NULL,NULL,NULL}'::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: ((unique1 IS NULL) AND (unique1 IS NULL))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+ unique1 
+---------
+(0 rows)
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
                                 QUERY PLAN                                 
@@ -2462,6 +2551,44 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 ---------
 (0 rows)
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > '-1'::integer) AND (thousand >= 0) AND (tenthous = 3000))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+(1 row)
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand < 3) AND (thousand <= 2) AND (tenthous = 1001))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        1 |     1001
+(1 row)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index b1d12585e..cf48ae6d0 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5332,9 +5332,10 @@ Function              | in_range(time without time zone,time without time zone,i
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql
index 670ad5c6e..68c61dbc7 100644
--- a/src/test/regress/sql/btree_index.sql
+++ b/src/test/regress/sql/btree_index.sql
@@ -327,6 +327,27 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 
 --
 -- Test for multilevel page deletion
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index be570da08..40ba3d65b 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -650,7 +650,9 @@ DROP TABLE syscol_table;
 --
 
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 
 SET enable_seqscan = OFF;
@@ -663,6 +665,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -675,6 +678,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NUL
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0, 1);
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -686,6 +690,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -697,6 +702,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -716,9 +722,9 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
   ORDER BY unique2 LIMIT 2;
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 5;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 3;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
 
@@ -852,7 +858,8 @@ SELECT count(*) FROM dupindexcols
   WHERE f1 BETWEEN 'WA' AND 'ZZZ' and id < 1000 and f1 ~<~ 'YX';
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 
 explain (costs off)
@@ -864,7 +871,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -874,7 +881,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -884,6 +891,15 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand DESC, tenthous DESC;
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -897,6 +913,21 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 
 SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('{33, 44}'::bigint[]);
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
 
@@ -942,6 +973,26 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
 SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1279b6942..748f77555 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -223,6 +223,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2740,6 +2741,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.49.0

v32-0005-DEBUG-Add-skip-scan-disable-GUCs.patchapplication/x-patch; name=v32-0005-DEBUG-Add-skip-scan-disable-GUCs.patchDownload
From 7d598ffb315a1bae300bbce42b24929469e43f33 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 18 Jan 2025 10:54:44 -0500
Subject: [PATCH v32 5/5] DEBUG: Add skip scan disable GUCs.

---
 src/include/access/nbtree.h                   |  5 +++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 37 +++++++++++++++++++
 src/backend/access/nbtree/nbtutils.c          |  3 ++
 src/backend/utils/misc/guc_tables.c           | 34 +++++++++++++++++
 4 files changed, 79 insertions(+)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 02da56995..ca4b6ead4 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1178,6 +1178,11 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+extern PGDLLIMPORT bool skipscan_iprefix_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 91d34ef96..a8e7c05d2 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -21,6 +21,33 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
+/*
+ * skipscan_iprefix_enabled can be used to disable optimizations used when the
+ * maintenance overhead of skip arrays stops paying for itself
+ */
+bool		skipscan_iprefix_enabled = true;
+
 typedef struct BTScanKeyPreproc
 {
 	ScanKey		inkey;
@@ -1650,6 +1677,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
 			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
 
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].sksup = NULL;
+
 			/*
 			 * We'll need a 3-way ORDER proc.  Set that up now.
 			 */
@@ -2169,6 +2200,12 @@ _bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
 		if (attno_has_rowcompare)
 			break;
 
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
 		/*
 		 * Now consider next attno_inkey (or keep going if this is an
 		 * additional scan key against the same attribute)
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 8f9e6050d..fc79f6700 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -2509,6 +2509,9 @@ _bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate)
 	if (so->numberOfKeys == 0)
 		return;
 
+	if (!skipscan_iprefix_enabled)
+		return;
+
 	/* minoff is an offset to the lowest non-pivot tuple on the page */
 	iid = PageGetItemId(pstate->page, pstate->minoff);
 	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 76c7c6bb4..34b860c20 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1788,6 +1789,28 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
+	/* XXX Remove before commit */
+	{
+		{"skipscan_iprefix_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_iprefix_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3724,6 +3747,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
-- 
2.49.0

In reply to: Peter Geoghegan (#86)
5 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Mar 28, 2025 at 5:59 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v32, which has very few changes, but does add a new patch:
a patch that adds skip-array-specific primitive index scan heuristics
to _bt_advance_array_keys (this is
v32-0003-Improve-skip-scan-primitive-scan-scheduling.patch).

Attached is v33, which:

* Polishes the recently introduced
0003-Improve-skip-scan-primitive-scan-scheduling.patch logic, and
tweaks related code in _bt_advance_array_keys.

This includes one bug fix affecting backwards scans, which no longer
reuse pstate.skip to stop reading tuples when so->scanBehind was set.
Now _bt_readpage's backwards scan loop tests so.scanBehind directly
and explicitly, instead (there's no such change to its forwards scan
loop, though).

In v32 we could accidentally set pstate.skip (which is mostly intended
to be used by the "look-ahead" optimization added by the Postgres 17
SAOP project) to 0 (InvalidOffsetNumber), which actually represents
"don't skip at all" -- that was wrong. This happened when the
"pstate.skip = pstate.minoffnum - 1" statement gave us
InvalidOffsetNumber because pstate.minoffnum was already 1
(FirstOffsetNumber).

(FWIW, this only affected backwards scans at the point that they read
the index's rightmost leaf page, since any other leaf page has to have
a high key at FirstOffSetNumber, which implies a pstate.minoffnum of
FirstOffsetNumber+1, which meant we set pstate.skip = 1
(FirstOffsetNumber) by subtraction, which accidentally failed to
fail).

* v33 also makes small tweaks and comment clean-ups to the logic in
and around _bt_set_startikey, added by 0002-*, with the goal
simplifying the code, and in particular making the possible impact of
pstate.forcenonrequired on maintenance of the scan's arrays clearer.

We must not break the rule established by the Postgres 17 SAOP work:
the scan's array keys should always track the scan's progress through
the index's key space (I refer to the rule explained by the 17 SAOP
commit's message, see commit
5bf748b86bc6786a3fc57fc7ce296c37da6564b0). While we do "temporarily
stop following that rule" within a given pstate.forcenonrequired call
to _bt_readpage (to save some cycles), that should never have lasting
side-effects; there should be no observable effect outside of
_bt_readpage itself. In other words, the use of
pstate.forcenonrequired by _bt_readpage should leave the scan's arrays
in exactly the same state as _bt_readpage would have left them had it
never used pstate.forcenonrequired mode to begin with.

(FWIW, I have no reason to believe that v32 had any bugs pertaining to
this. AFAICT it didn't actually break "the general rule established by
the Postgres 17 SAOP work", but the explanation for why that was so
was needlessly complicated.)

I'm now very close to committing everything. Though I do still want
another pair of eyes on the newer
0003-Improve-skip-scan-primitive-scan-scheduling.patch stuff before
commiting (since I still intend to commit all the remaining patches
together).

--
Peter Geoghegan

Attachments:

v33-0003-Improve-skip-scan-primitive-scan-scheduling.patchapplication/octet-stream; name=v33-0003-Improve-skip-scan-primitive-scan-scheduling.patchDownload
From bd9cbb88abbcf5ded55cd69bdb80d272e666f2e7 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 26 Mar 2025 18:21:27 -0400
Subject: [PATCH v33 3/5] Improve skip scan primitive scan scheduling.

Fixes a few remaining cases where affected skip scans never quite manage
to reach the point of being able to apply the "passed first page"
heuristic added by commit 9a2e2a28.  They only need to manage to get
there once to converge on full index scan behavior, but it was still
possible for that to never happen, with the wrong workload.

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=RVdG3zWytFWBsyW7fWH7zveFvTHed5JKEsuTT0RCO_A@mail.gmail.com
---
 src/include/access/nbtree.h           |  3 +-
 src/backend/access/nbtree/nbtsearch.c | 16 +++++
 src/backend/access/nbtree/nbtutils.c  | 90 ++++++++++++++++++---------
 3 files changed, 78 insertions(+), 31 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index c8708f2fd..02da56995 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1118,10 +1118,11 @@ typedef struct BTReadPageState
 
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
-	 * (only used during scans with array keys)
+	 * and primscan scheduling (only used during scans with array keys)
 	 */
 	int16		rechecks;
 	int16		targetdistance;
+	int16		nskipadvances;
 
 } BTReadPageState;
 
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index e95c396d2..a653b8d2f 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1655,6 +1655,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.continuescan = true; /* default assumption */
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
+	pstate.nskipadvances = 0;
 
 	if (ScanDirectionIsForward(dir))
 	{
@@ -1884,6 +1885,21 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
+			if (arrayKeys && so->scanBehind)
+			{
+				/*
+				 * Done scanning this page, but not done with the current
+				 * primscan.
+				 *
+				 * Note: Forward scans don't check this explicitly, since they
+				 * prefer to reuse pstate.skip for this instead.
+				 */
+				Assert(!passes_quals && pstate.continuescan);
+				Assert(!pstate.forcenonrequired);
+
+				break;
+			}
+
 			/*
 			 * Check if we need to skip ahead to a later tuple (only possible
 			 * when the scan uses array keys)
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index ea5b3b688..2d060e185 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -26,6 +26,7 @@
 
 #define LOOK_AHEAD_REQUIRED_RECHECKS 	3
 #define LOOK_AHEAD_DEFAULT_DISTANCE 	5
+#define NSKIPADVANCES_THRESHOLD			3
 
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
@@ -41,7 +42,8 @@ static void _bt_array_set_low_or_high(Relation rel, ScanKey skey,
 									  BTArrayKeyInfo *array, bool low_not_high);
 static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
-static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
+static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir,
+											 bool *skip_array_set);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 										 IndexTuple tuple, TupleDesc tupdesc, int tupnatts,
@@ -970,7 +972,8 @@ _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
  * advanced (every array remains at its final element for scan direction).
  */
 static bool
-_bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
+_bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir,
+								 bool *skip_array_set)
 {
 	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
@@ -985,6 +988,9 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 		BTArrayKeyInfo *array = &so->arrayKeys[i];
 		ScanKey		skey = &so->keyData[array->scan_key];
 
+		if (array->num_elems == -1)
+			*skip_array_set = true;
+
 		if (ScanDirectionIsForward(dir))
 		{
 			if (_bt_array_increment(rel, skey, array))
@@ -1460,6 +1466,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	ScanDirection dir = so->currPos.dir;
 	int			arrayidx = 0;
 	bool		beyond_end_advance = false,
+				skip_array_advanced = false,
 				has_required_opposite_direction_only = false,
 				all_required_satisfied = true,
 				all_satisfied = true;
@@ -1756,6 +1763,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 				/* Skip array's new element is tupdatum (or MINVAL/MAXVAL) */
 				_bt_skiparray_set_element(rel, cur, array, result,
 										  tupdatum, tupnull);
+				skip_array_advanced = true;
 			}
 			else if (array->cur_elem != set_elem)
 			{
@@ -1772,11 +1780,19 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * higher-order arrays (might exhaust all the scan's arrays instead, which
 	 * ends the top-level scan).
 	 */
-	if (beyond_end_advance && !_bt_advance_array_keys_increment(scan, dir))
+	if (beyond_end_advance &&
+		!_bt_advance_array_keys_increment(scan, dir, &skip_array_advanced))
 		goto end_toplevel_scan;
 
 	Assert(_bt_verify_keys_with_arraykeys(scan));
 
+	/*
+	 * Maintain a page-level count of the number of times the scan's array
+	 * keys advanced in a way that affected at least one skip array
+	 */
+	if (sktrig_required && skip_array_advanced)
+		pstate->nskipadvances++;
+
 	/*
 	 * Does tuple now satisfy our new qual?  Recheck with _bt_check_compare.
 	 *
@@ -1946,26 +1962,12 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * Being pessimistic would also give some scans with non-required arrays a
 	 * perverse advantage over similar scans that use required arrays instead.
 	 *
-	 * You can think of this as a speculative bet on what the scan is likely
-	 * to find on the next page.  It's not much of a gamble, though, since the
-	 * untruncated prefix of attributes must strictly satisfy the new qual.
+	 * This is similar to our scan-level heuristics, below.  They also set
+	 * scanBehind to speculatively continue the primscan onto the next page.
 	 */
 	if (so->scanBehind)
 	{
-		/*
-		 * Truncated high key -- _bt_scanbehind_checkkeys recheck scheduled.
-		 *
-		 * Remember if recheck needs to call _bt_oppodir_checkkeys for next
-		 * page's finaltup (see below comments about "Handle inequalities
-		 * marked required in the opposite scan direction" for why).
-		 */
-		so->oppositeDirCheck = has_required_opposite_direction_only;
-
-		/*
-		 * Make sure that any SAOP arrays that were not marked required by
-		 * preprocessing are reset to their first element for this direction
-		 */
-		_bt_rewind_nonrequired_arrays(scan, dir);
+		/* Truncated high key -- _bt_scanbehind_checkkeys recheck scheduled */
 	}
 
 	/*
@@ -2006,6 +2008,10 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	else if (has_required_opposite_direction_only && pstate->finaltup &&
 			 unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup)))
 	{
+		/*
+		 * Make sure that any SAOP arrays that were not marked required by
+		 * preprocessing are reset to their first element for this direction
+		 */
 		_bt_rewind_nonrequired_arrays(scan, dir);
 		goto new_prim_scan;
 	}
@@ -2032,11 +2038,21 @@ continue_scan:
 
 	if (so->scanBehind)
 	{
-		/* Optimization: skip by setting "look ahead" mechanism's offnum */
+		/*
+		 * Remember if recheck needs to call _bt_oppodir_checkkeys for next
+		 * page's finaltup (see above comments about "Handle inequalities
+		 * marked required in the opposite scan direction" for why).
+		 */
+		so->oppositeDirCheck = has_required_opposite_direction_only;
+
+		_bt_rewind_nonrequired_arrays(scan, dir);
+
+		/*
+		 * skip by setting "look ahead" mechanism's offnum for forwards scans
+		 * (backwards scans check scanBehind flag directly instead)
+		 */
 		if (ScanDirectionIsForward(dir))
 			pstate->skip = pstate->maxoff + 1;
-		else
-			pstate->skip = pstate->minoff - 1;
 	}
 
 	/* Caller's tuple doesn't match the new qual */
@@ -2059,19 +2075,31 @@ new_prim_scan:
 	 * This will in turn encourage _bt_readpage to apply the pstate.startikey
 	 * optimization more often.
 	 *
-	 * Note: This heuristic isn't as aggressive as you might think.  We're
+	 * Also continue the ongoing primitive index scan when it is still on the
+	 * first page if there have been more than NSKIPADVANCES_THRESHOLD calls
+	 * here that each advanced at least one of the scan's skip arrays
+	 * (deliberately ignore advancements that only affected SAOP arrays here).
+	 * A page that cycles through this many skip array elements is quite
+	 * likely to neighbor similar pages, that we'll also need to read.
+	 *
+	 * Note: These heuristics aren't as aggressive as you might think.  We're
 	 * conservative about allowing a primitive scan to step from the first
 	 * leaf page it reads to the page's sibling page (we only allow it on
-	 * first pages whose finaltup strongly suggests that it'll work out).
+	 * first pages whose finaltup strongly suggests that it'll work out, as
+	 * well as first pages that have a large number of skip array advances).
 	 * Clearing this first page finaltup hurdle is a strong signal in itself.
+	 *
+	 * Note: The NSKIPADVANCES_THRESHOLD heuristic exists only to avoid
+	 * pathological cases.  Specifically, cases where a skip scan should just
+	 * behave like a traditional full index scan, but ends up "skipping" again
+	 * and again, descending to the prior leaf page's direct sibling leaf page
+	 * each time.  This misbehavior would otherwise be possible during scans
+	 * that never quite manage to "clear the first page finaltup hurdle".
 	 */
-	if (!pstate->firstpage)
+	if (!pstate->firstpage || pstate->nskipadvances > NSKIPADVANCES_THRESHOLD)
 	{
 		/* Schedule a recheck once on the next (or previous) page */
 		so->scanBehind = true;
-		so->oppositeDirCheck = has_required_opposite_direction_only;
-
-		_bt_rewind_nonrequired_arrays(scan, dir);
 
 		/* Continue the current primitive scan after all */
 		goto continue_scan;
@@ -2443,7 +2471,9 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  * the first page (first for the current primitive scan) avoids wasting cycles
  * during selective point queries.  They typically don't stand to gain as much
  * when we can set pstate.startikey, and are likely to notice the overhead of
- * calling here.
+ * calling here.  (Also, allowing pstate.forcenonrequired to be set on a
+ * primscan's first page would mislead _bt_advance_array_keys, which expects
+ * pstate.nskipadvances to be representative of any first page's key space .)
  *
  * Caller must reset startikey and forcenonrequired ahead of the _bt_checkkeys
  * call for pstate.finaltup iff we set forcenonrequired=true.  This will give
-- 
2.49.0

v33-0004-Apply-low-order-skip-key-in-_bt_first-more-often.patchapplication/octet-stream; name=v33-0004-Apply-low-order-skip-key-in-_bt_first-more-often.patchDownload
From 03ee8da068824218632b3b552a0bd30354032d63 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 13 Jan 2025 16:08:32 -0500
Subject: [PATCH v33 4/5] Apply low-order skip key in _bt_first more often.

Convert low_compare and high_compare nbtree skip array inequalities
(with opclasses that offer skip support) such that _bt_first is
consistently able to use later keys when descending the tree within
_bt_first.

For example, an index qual "WHERE a > 5 AND b = 2" is now converted to
"WHERE a >= 6 AND b = 2" by a new preprocessing step that takes place
after a final low_compare and/or high_compare are chosen by all earlier
preprocessing steps.  That way the scan's initial call to _bt_first will
use "WHERE a >= 6 AND b = 2" to find the initial leaf level position,
rather than merely using "WHERE a > 5" -- "b = 2" can always be applied.
This has a decent chance of making the scan avoid an extra _bt_first
call that would otherwise be needed just to determine the lowest-sorting
"a" value in the index (the lowest that still satisfies "WHERE a > 5").

The transformation process can only lower the total number of index
pages read when the use of a more restrictive set of initial positioning
keys in _bt_first actually allows the scan to land on some later leaf
page directly, relative to the unoptimized case (or on an earlier leaf
page directly, when scanning backwards).  The savings can be far greater
when affected skip arrays come after some higher-order array.  For
example, a qual "WHERE x IN (1, 2, 3) AND y > 5 AND z = 2" can now save
as many as 3 _bt_first calls as a result of these transformations (there
can be as many as 1 _bt_first call saved per "x" array element).

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Discussion: https://postgr.es/m/CAH2-Wz=FJ78K3WsF3iWNxWnUCY9f=Jdg3QPxaXE=uYUbmuRz5Q@mail.gmail.com
---
 src/backend/access/nbtree/nbtpreprocesskeys.c | 180 ++++++++++++++++++
 src/test/regress/expected/create_index.out    |  21 ++
 src/test/regress/sql/create_index.sql         |  10 +
 3 files changed, 211 insertions(+)

diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 339092dfa..0947eb0a6 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -50,6 +50,12 @@ static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
 								 BTArrayKeyInfo *array, bool *qual_ok);
 static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
 								 BTArrayKeyInfo *array, bool *qual_ok);
+static void _bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+									   BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
+static void _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+										  BTArrayKeyInfo *array);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
 static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
@@ -1295,6 +1301,171 @@ _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
 	return true;
 }
 
+/*
+ * Applies the opfamily's skip support routine to convert the skip array's >
+ * low_compare key (if any) into a >= key, and to convert its < high_compare
+ * key (if any) into a <= key.  Decrements the high_compare key's sk_argument,
+ * and/or increments the low_compare key's sk_argument (also adjusts their
+ * operator strategies, while changing the operator as appropriate).
+ *
+ * This optional optimization reduces the number of descents required within
+ * _bt_first.  Whenever _bt_first is called with a skip array whose current
+ * array element is the sentinel value MINVAL, using a transformed >= key
+ * instead of using the original > key makes it safe to include lower-order
+ * scan keys in the insertion scan key (there must be lower-order scan keys
+ * after the skip array).  We will avoid an extra _bt_first to find the first
+ * value in the index > sk_argument -- at least when the first real matching
+ * value in the index happens to be an exact match for the sk_argument value
+ * that we produced here by incrementing the original input key's sk_argument.
+ * (Backwards scans derive the same benefit when they encounter the sentinel
+ * value MAXVAL, by converting the high_compare key from < to <=.)
+ *
+ * Note: The transformation is only correct when it cannot allow the scan to
+ * overlook matching tuples, but we don't have enough semantic information to
+ * safely make sure that can't happen during scans with cross-type operators.
+ * That's why we'll never apply the transformation in cross-type scenarios.
+ * For example, if we attempted to convert "sales_ts > '2024-01-01'::date"
+ * into "sales_ts >= '2024-01-02'::date" given a "sales_ts" attribute whose
+ * input opclass is timestamp_ops, the scan would overlook _all_ tuples for
+ * sales that fell on '2024-01-01'.
+ *
+ * Note: We can safely modify array->low_compare/array->high_compare in place
+ * because they just point to copies of our scan->keyData[] input scan keys
+ * (namely the copies returned by _bt_preprocess_array_keys to be used as
+ * input into the standard preprocessing steps in _bt_preprocess_keys).
+ * Everything will be reset if there's a rescan.
+ */
+static void
+_bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk,
+						   BTArrayKeyInfo *array)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	MemoryContext oldContext;
+
+	/*
+	 * Called last among all preprocessing steps, when the skip array's final
+	 * low_compare and high_compare have both been chosen
+	 */
+	Assert(arraysk->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1 && !array->null_elem && array->sksup);
+
+	oldContext = MemoryContextSwitchTo(so->arrayContext);
+
+	if (array->high_compare &&
+		array->high_compare->sk_strategy == BTLessStrategyNumber)
+		_bt_skiparray_strat_decrement(scan, arraysk, array);
+
+	if (array->low_compare &&
+		array->low_compare->sk_strategy == BTGreaterStrategyNumber)
+		_bt_skiparray_strat_increment(scan, arraysk, array);
+
+	MemoryContextSwitchTo(oldContext);
+}
+
+/*
+ * Convert skip array's > low_compare key into a >= key
+ */
+static void
+_bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				leop;
+	RegProcedure cmp_proc;
+	ScanKey		high_compare = array->high_compare;
+	Datum		orig_sk_argument = high_compare->sk_argument,
+				new_sk_argument;
+	bool		uflow;
+
+	Assert(high_compare->sk_strategy == BTLessStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (high_compare->sk_subtype != opcintype &&
+		high_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Decrement, handling underflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->decrement(rel, orig_sk_argument, &uflow);
+	if (uflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up <= operator (might fail) */
+	leop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTLessEqualStrategyNumber);
+	if (!OidIsValid(leop))
+		return;
+	cmp_proc = get_opcode(leop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform < high_compare key into <= key */
+		fmgr_info(cmp_proc, &high_compare->sk_func);
+		high_compare->sk_argument = new_sk_argument;
+		high_compare->sk_strategy = BTLessEqualStrategyNumber;
+	}
+}
+
+/*
+ * Convert skip array's < low_compare key into a <= key
+ */
+static void
+_bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk,
+							  BTArrayKeyInfo *array)
+{
+	Relation	rel = scan->indexRelation;
+	Oid			opfamily = rel->rd_opfamily[arraysk->sk_attno - 1],
+				opcintype = rel->rd_opcintype[arraysk->sk_attno - 1],
+				geop;
+	RegProcedure cmp_proc;
+	ScanKey		low_compare = array->low_compare;
+	Datum		orig_sk_argument = low_compare->sk_argument,
+				new_sk_argument;
+	bool		oflow;
+
+	Assert(low_compare->sk_strategy == BTGreaterStrategyNumber);
+
+	/*
+	 * Only perform the transformation when the operator type matches the
+	 * index attribute's input opclass type
+	 */
+	if (low_compare->sk_subtype != opcintype &&
+		low_compare->sk_subtype != InvalidOid)
+		return;
+
+	/* Increment, handling overflow by marking the qual unsatisfiable */
+	new_sk_argument = array->sksup->increment(rel, orig_sk_argument, &oflow);
+	if (oflow)
+	{
+		BTScanOpaque so = (BTScanOpaque) scan->opaque;
+
+		so->qual_ok = false;
+		return;
+	}
+
+	/* Look up >= operator (might fail) */
+	geop = get_opfamily_member(opfamily, opcintype, opcintype,
+							   BTGreaterEqualStrategyNumber);
+	if (!OidIsValid(geop))
+		return;
+	cmp_proc = get_opcode(geop);
+	if (RegProcedureIsValid(cmp_proc))
+	{
+		/* Transform > low_compare key into >= key */
+		fmgr_info(cmp_proc, &low_compare->sk_func);
+		low_compare->sk_argument = new_sk_argument;
+		low_compare->sk_strategy = BTGreaterEqualStrategyNumber;
+	}
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1838,6 +2009,15 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 				}
 				else
 				{
+					/*
+					 * Any skip array low_compare and high_compare scan keys
+					 * are now final.  Transform the array's > low_compare key
+					 * into a >= key (and < high_compare keys into a <= key).
+					 */
+					if (array->num_elems == -1 && array->sksup &&
+						!array->null_elem)
+						_bt_skiparray_strat_adjust(scan, outkey, array);
+
 					/* Match found, so done with this array */
 					arrayidx++;
 				}
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 2cfb26699..9ade7b835 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2589,6 +2589,27 @@ ORDER BY thousand;
         1 |     1001
 (1 row)
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+                                            QUERY PLAN                                            
+--------------------------------------------------------------------------------------------------
+ Limit
+   ->  Index Only Scan using tenk1_thous_tenthous on tenk1
+         Index Cond: ((thousand > '-1'::integer) AND (tenthous = ANY ('{1001,3000}'::integer[])))
+(3 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+        1 |     1001
+(2 rows)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index cd90b1c3a..e21ff4265 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -993,6 +993,16 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
 ORDER BY thousand;
 
+-- Skip array preprocessing increments "thousand > -1" to  "thousand >= 0"
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 AND tenthous IN (1001,3000)
+ORDER BY thousand limit 2;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
-- 
2.49.0

v33-0005-DEBUG-Add-skip-scan-disable-GUCs.patchapplication/octet-stream; name=v33-0005-DEBUG-Add-skip-scan-disable-GUCs.patchDownload
From 545412ed16f77e0ad73295a28123c6ea4fd28a7f Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 18 Jan 2025 10:54:44 -0500
Subject: [PATCH v33 5/5] DEBUG: Add skip scan disable GUCs.

---
 src/include/access/nbtree.h                   |  5 +++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 37 +++++++++++++++++++
 src/backend/access/nbtree/nbtutils.c          |  3 ++
 src/backend/utils/misc/guc_tables.c           | 34 +++++++++++++++++
 4 files changed, 79 insertions(+)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 02da56995..ca4b6ead4 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1178,6 +1178,11 @@ typedef struct BTOptions
 #define PROGRESS_BTREE_PHASE_PERFORMSORT_2				4
 #define PROGRESS_BTREE_PHASE_LEAF_LOAD					5
 
+/* GUC parameters (just a temporary convenience for reviewers) */
+extern PGDLLIMPORT int skipscan_prefix_cols;
+extern PGDLLIMPORT bool skipscan_skipsupport_enabled;
+extern PGDLLIMPORT bool skipscan_iprefix_enabled;
+
 /*
  * external entry points for btree, in nbtree.c
  */
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 0947eb0a6..6501ef620 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -21,6 +21,33 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+/*
+ * GUC parameters (temporary convenience for reviewers).
+ *
+ * To disable all skipping, set skipscan_prefix_cols=0.  Otherwise set it to
+ * the attribute number that you wish to make the last attribute number that
+ * we can add a skip scan key for.  For example, skipscan_prefix_cols=1 makes
+ * an index scan with qual "WHERE b = 1 AND d = 42" generate a skip scan key
+ * on the column 'a' (which is attnum 1) only, preventing us from adding one
+ * for the column 'c'.  And so only the scan key on 'b' (not the one on 'd')
+ * gets marked required within _bt_preprocess_keys -- there is no 'c' skip
+ * array to "anchor the required-ness" of 'b' through 'c' into 'd'.
+ */
+int			skipscan_prefix_cols = INDEX_MAX_KEYS;
+
+/*
+ * skipscan_skipsupport_enabled can be used to avoid using skip support.  Used
+ * to quantify the performance benefit that comes from having dedicated skip
+ * support, with a given opclass and test query.
+ */
+bool		skipscan_skipsupport_enabled = true;
+
+/*
+ * skipscan_iprefix_enabled can be used to disable optimizations used when the
+ * maintenance overhead of skip arrays stops paying for itself
+ */
+bool		skipscan_iprefix_enabled = true;
+
 typedef struct BTScanKeyPreproc
 {
 	ScanKey		inkey;
@@ -1650,6 +1677,10 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
 			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
 
+			/* Temporary testing GUC can disable the use of skip support */
+			if (!skipscan_skipsupport_enabled)
+				so->arrayKeys[numArrayKeys].sksup = NULL;
+
 			/*
 			 * We'll need a 3-way ORDER proc.  Set that up now.
 			 */
@@ -2175,6 +2206,12 @@ _bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
 		if (attno_has_rowcompare)
 			break;
 
+		/*
+		 * Apply temporary testing GUC that can be used to disable skipping
+		 */
+		if (attno_inkey > skipscan_prefix_cols)
+			break;
+
 		/*
 		 * Now consider next attno_inkey (or keep going if this is an
 		 * additional scan key against the same attribute)
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 2d060e185..38f79983f 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -2505,6 +2505,9 @@ _bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate)
 	if (so->numberOfKeys == 0)
 		return;
 
+	if (!skipscan_iprefix_enabled)
+		return;
+
 	/* minoff is an offset to the lowest non-pivot tuple on the page */
 	iid = PageGetItemId(pstate->page, pstate->minoff);
 	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 4eaeca89f..eb46c24c4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -28,6 +28,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/nbtree.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -1788,6 +1789,28 @@ struct config_bool ConfigureNamesBool[] =
 	},
 #endif
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_skipsupport_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_skipsupport_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
+	/* XXX Remove before commit */
+	{
+		{"skipscan_iprefix_enabled", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_iprefix_enabled,
+		true,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows whether datetimes are integer based."),
@@ -3724,6 +3747,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	/* XXX Remove before commit */
+	{
+		{"skipscan_prefix_cols", PGC_SUSET, DEVELOPER_OPTIONS,
+			NULL, NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&skipscan_prefix_cols,
+		INDEX_MAX_KEYS, 0, INDEX_MAX_KEYS,
+		NULL, NULL, NULL
+	},
+
 	{
 		/* Can't be set in postgresql.conf */
 		{"server_version_num", PGC_INTERNAL, PRESET_OPTIONS,
-- 
2.49.0

v33-0001-Add-nbtree-skip-scan-optimizations.patchapplication/octet-stream; name=v33-0001-Add-nbtree-skip-scan-optimizations.patchDownload
From d91a98dd1bce6ffd0aeacf703c5e52f4db0e8880 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 16 Apr 2024 13:21:36 -0400
Subject: [PATCH v33 1/5] Add nbtree skip scan optimizations.

Teach nbtree composite index scans to opportunistically skip over
irrelevant sections of composite indexes given a query with an omitted
prefix column.  When nbtree is passed input scan keys derived from a
query predicate "WHERE b = 5", new nbtree preprocessing steps now output
"WHERE a = ANY(<every possible 'a' value>) AND b = 5" scan keys.  That
is, preprocessing generates a "skip array" (along with an associated
scan key) for the omitted column "a", which makes it safe to mark the
scan key on "b" as required to continue the scan.  This is far more
efficient than a traditional full index scan whenever it allows the scan
to skip over many irrelevant leaf pages, by iteratively repositioning
itself using the keys on "a" and "b" together.

A skip array has "elements" that are generated procedurally and on
demand, but otherwise works just like a regular ScalarArrayOp array.
Preprocessing can freely add a skip array before or after any input
ScalarArrayOp arrays.  Index scans with a skip array decide when and
where to reposition the scan using the same approach as any other scan
with array keys.  This design builds on the design for array advancement
and primitive scan scheduling added to Postgres 17 by commit 5bf748b8.

The core B-Tree operator classes on most discrete types generate their
array elements with the help of their own custom skip support routine.
This infrastructure gives nbtree a way to generate the next required
array element by incrementing (or decrementing) the current array value.
It can reduce the number of index descents in cases where the next
possible indexable value frequently turns out to be the next value
stored in the index.  Opclasses that lack a skip support routine fall
back on having nbtree "increment" (or "decrement") a skip array's
current element by setting the NEXT (or PRIOR) scan key flag, without
directly changing the scan key's sk_argument.  These sentinel values
behave just like any other value from an array -- though they can never
locate equal index tuples (they can only locate the next group of index
tuples containing the next set of non-sentinel values that the scan's
arrays need to advance to).

Inequality scan keys can affect how skip arrays generate their values.
Their range is constrained by the inequalities.  For example, a skip
array on "a" will only use element values 1 and 2 given a qual such as
"WHERE a BETWEEN 1 AND 2 AND b = 66".  A scan using such a skip array
has almost identical performance characteristics to one with the qual
"WHERE a = ANY('{1, 2}') AND b = 66".  The scan will be much faster when
it can be executed as two selective primitive index scans instead of a
single very large index scan that reads many irrelevant leaf pages.
However, the array transformation process won't always lead to improved
performance at runtime.  Much depends on physical index characteristics.

B-Tree preprocessing is optimistic about skipping working out: it
applies static, generic rules when determining where to generate skip
arrays, which assumes that the runtime overhead of maintaining skip
arrays will pay for itself -- or lead to only a modest performance loss.
As things stand, these assumptions are much too optimistic: skip array
maintenance will lead to unacceptable regressions with unsympathetic
queries (queries whose scan can't skip over many irrelevant leaf pages).
An upcoming commit will address the problems in this area by enhancing
_bt_readpage's approach to saving cycles on scan key evaluation, making
it work in a way that directly considers the needs of = array keys
(particularly = skip array keys).

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Masahiro Ikeda <masahiro.ikeda@nttdata.com>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Tomas Vondra <tomas@vondra.me>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Reviewed-By: Aleksander Alekseev <aleksander@timescale.com>
Reviewed-By: Alena Rybakina <a.rybakina@postgrespro.ru>
Discussion: https://postgr.es/m/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
---
 src/include/access/amapi.h                    |   3 +-
 src/include/access/nbtree.h                   |  34 +-
 src/include/catalog/pg_amproc.dat             |  22 +
 src/include/catalog/pg_proc.dat               |  27 +
 src/include/utils/skipsupport.h               |  98 +++
 src/backend/access/index/indexam.c            |   3 +-
 src/backend/access/nbtree/nbtcompare.c        | 273 +++++++
 src/backend/access/nbtree/nbtpreprocesskeys.c | 628 +++++++++++++--
 src/backend/access/nbtree/nbtree.c            | 196 ++++-
 src/backend/access/nbtree/nbtsearch.c         | 130 ++-
 src/backend/access/nbtree/nbtutils.c          | 756 ++++++++++++++++--
 src/backend/access/nbtree/nbtvalidate.c       |   4 +
 src/backend/commands/opclasscmds.c            |  25 +
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/date.c                  |  46 ++
 src/backend/utils/adt/meson.build             |   1 +
 src/backend/utils/adt/selfuncs.c              | 491 +++++++++---
 src/backend/utils/adt/skipsupport.c           |  61 ++
 src/backend/utils/adt/timestamp.c             |  48 ++
 src/backend/utils/adt/uuid.c                  |  70 ++
 doc/src/sgml/btree.sgml                       |  34 +-
 doc/src/sgml/indexam.sgml                     |   3 +-
 doc/src/sgml/indices.sgml                     |  49 +-
 doc/src/sgml/monitoring.sgml                  |   4 +-
 doc/src/sgml/perform.sgml                     |  31 +
 doc/src/sgml/xindex.sgml                      |  16 +-
 src/test/regress/expected/alter_generic.out   |  10 +-
 src/test/regress/expected/btree_index.out     |  41 +
 src/test/regress/expected/create_index.out    | 183 ++++-
 src/test/regress/expected/psql.out            |   3 +-
 src/test/regress/sql/alter_generic.sql        |   5 +-
 src/test/regress/sql/btree_index.sql          |  21 +
 src/test/regress/sql/create_index.sql         |  63 +-
 src/tools/pgindent/typedefs.list              |   3 +
 34 files changed, 2998 insertions(+), 385 deletions(-)
 create mode 100644 src/include/utils/skipsupport.h
 create mode 100644 src/backend/utils/adt/skipsupport.c

diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c4a073773..52916bab7 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -214,7 +214,8 @@ typedef void (*amrestrpos_function) (IndexScanDesc scan);
  */
 
 /* estimate size of parallel scan descriptor */
-typedef Size (*amestimateparallelscan_function) (int nkeys, int norderbys);
+typedef Size (*amestimateparallelscan_function) (Relation indexRelation,
+												 int nkeys, int norderbys);
 
 /* prepare for parallel index scan */
 typedef void (*aminitparallelscan_function) (void *target);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index faabcb78e..b86bf7bf3 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -24,6 +24,7 @@
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
+#include "utils/skipsupport.h"
 
 /* There's room for a 16-bit vacuum cycle ID in BTPageOpaqueData */
 typedef uint16 BTCycleId;
@@ -707,6 +708,10 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
  *	(BTOPTIONS_PROC).  These procedures define a set of user-visible
  *	parameters that can be used to control operator class behavior.  None of
  *	the built-in B-Tree operator classes currently register an "options" proc.
+ *
+ *	To facilitate more efficient B-Tree skip scans, an operator class may
+ *	choose to offer a sixth amproc procedure (BTSKIPSUPPORT_PROC).  For full
+ *	details, see src/include/utils/skipsupport.h.
  */
 
 #define BTORDER_PROC		1
@@ -714,7 +719,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
-#define BTNProcs			5
+#define BTSKIPSUPPORT_PROC	6
+#define BTNProcs			6
 
 /*
  *	We need to be able to tell the difference between read and write
@@ -1027,10 +1033,21 @@ typedef BTScanPosData *BTScanPos;
 /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
 typedef struct BTArrayKeyInfo
 {
+	/* fields used by both kinds of array (standard arrays and skip arrays) */
 	int			scan_key;		/* index of associated key in keyData */
-	int			cur_elem;		/* index of current element in elem_values */
-	int			num_elems;		/* number of elems in current array value */
+	int			num_elems;		/* number of elems (-1 for skip array) */
+
+	/* fields for ScalarArrayOpExpr arrays */
 	Datum	   *elem_values;	/* array of num_elems Datums */
+	int			cur_elem;		/* index of current element in elem_values */
+
+	/* fields for skip arrays, which generate element datums procedurally */
+	int16		attlen;			/* attr's length, in bytes */
+	bool		attbyval;		/* attr's FormData_pg_attribute.attbyval */
+	bool		null_elem;		/* NULL is lowest/highest element? */
+	SkipSupport sksup;			/* skip support (NULL if opclass lacks it) */
+	ScanKey		low_compare;	/* array's > or >= lower bound */
+	ScanKey		high_compare;	/* array's < or <= upper bound */
 } BTArrayKeyInfo;
 
 typedef struct BTScanOpaqueData
@@ -1119,6 +1136,15 @@ typedef struct BTReadPageState
  */
 #define SK_BT_REQFWD	0x00010000	/* required to continue forward scan */
 #define SK_BT_REQBKWD	0x00020000	/* required to continue backward scan */
+#define SK_BT_SKIP		0x00040000	/* skip array on column without input = */
+
+/* SK_BT_SKIP-only flags (set and unset by array advancement) */
+#define SK_BT_MINVAL	0x00080000	/* invalid sk_argument, use low_compare */
+#define SK_BT_MAXVAL	0x00100000	/* invalid sk_argument, use high_compare */
+#define SK_BT_NEXT		0x00200000	/* positions the scan > sk_argument */
+#define SK_BT_PRIOR		0x00400000	/* positions the scan < sk_argument */
+
+/* Remaps pg_index flag bits to uppermost SK_BT_* byte */
 #define SK_BT_INDOPTION_SHIFT  24	/* must clear the above bits */
 #define SK_BT_DESC			(INDOPTION_DESC << SK_BT_INDOPTION_SHIFT)
 #define SK_BT_NULLS_FIRST	(INDOPTION_NULLS_FIRST << SK_BT_INDOPTION_SHIFT)
@@ -1165,7 +1191,7 @@ extern bool btinsert(Relation rel, Datum *values, bool *isnull,
 					 bool indexUnchanged,
 					 struct IndexInfo *indexInfo);
 extern IndexScanDesc btbeginscan(Relation rel, int nkeys, int norderbys);
-extern Size btestimateparallelscan(int nkeys, int norderbys);
+extern Size btestimateparallelscan(Relation rel, int nkeys, int norderbys);
 extern void btinitparallelscan(void *target);
 extern bool btgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 19100482b..37000639f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,6 +21,8 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
+{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
+  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -41,12 +43,16 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -60,6 +66,9 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -74,6 +83,9 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
+{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -122,6 +134,8 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -141,6 +155,8 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -160,6 +176,8 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -193,6 +211,8 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -261,6 +281,8 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 8b68b16d7..8721f6e11 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1004,18 +1004,27 @@
 { oid => '3129', descr => 'sort support',
   proname => 'btint2sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint2sortsupport' },
+{ oid => '9290', descr => 'skip support',
+  proname => 'btint2skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint2skipsupport' },
 { oid => '351', descr => 'less-equal-greater',
   proname => 'btint4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int4 int4', prosrc => 'btint4cmp' },
 { oid => '3130', descr => 'sort support',
   proname => 'btint4sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint4sortsupport' },
+{ oid => '9291', descr => 'skip support',
+  proname => 'btint4skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint4skipsupport' },
 { oid => '842', descr => 'less-equal-greater',
   proname => 'btint8cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'int8 int8', prosrc => 'btint8cmp' },
 { oid => '3131', descr => 'sort support',
   proname => 'btint8sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btint8sortsupport' },
+{ oid => '9292', descr => 'skip support',
+  proname => 'btint8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btint8skipsupport' },
 { oid => '354', descr => 'less-equal-greater',
   proname => 'btfloat4cmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'float4 float4', prosrc => 'btfloat4cmp' },
@@ -1034,12 +1043,18 @@
 { oid => '3134', descr => 'sort support',
   proname => 'btoidsortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidsortsupport' },
+{ oid => '9293', descr => 'skip support',
+  proname => 'btoidskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoidskipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
 { oid => '358', descr => 'less-equal-greater',
   proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'char char', prosrc => 'btcharcmp' },
+{ oid => '9294', descr => 'skip support',
+  proname => 'btcharskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btcharskipsupport' },
 { oid => '359', descr => 'less-equal-greater',
   proname => 'btnamecmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'name name', prosrc => 'btnamecmp' },
@@ -2288,6 +2303,9 @@
 { oid => '3136', descr => 'sort support',
   proname => 'date_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'date_sortsupport' },
+{ oid => '9295', descr => 'skip support',
+  proname => 'date_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'date_skipsupport' },
 { oid => '4133', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
   proargtypes => 'date date interval bool bool',
@@ -4485,6 +4503,9 @@
 { oid => '1693', descr => 'less-equal-greater',
   proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'bool bool', prosrc => 'btboolcmp' },
+{ oid => '9296', descr => 'skip support',
+  proname => 'btboolskipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btboolskipsupport' },
 
 { oid => '1688', descr => 'hash',
   proname => 'time_hash', prorettype => 'int4', proargtypes => 'time',
@@ -6364,6 +6385,9 @@
 { oid => '3137', descr => 'sort support',
   proname => 'timestamp_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'timestamp_sortsupport' },
+{ oid => '9297', descr => 'skip support',
+  proname => 'timestamp_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'timestamp_skipsupport' },
 
 { oid => '4134', descr => 'window RANGE support',
   proname => 'in_range', prorettype => 'bool',
@@ -9419,6 +9443,9 @@
 { oid => '3300', descr => 'sort support',
   proname => 'uuid_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'uuid_sortsupport' },
+{ oid => '9298', descr => 'skip support',
+  proname => 'uuid_skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'uuid_skipsupport' },
 { oid => '2961', descr => 'I/O',
   proname => 'uuid_recv', prorettype => 'uuid', proargtypes => 'internal',
   prosrc => 'uuid_recv' },
diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h
new file mode 100644
index 000000000..bc51847cf
--- /dev/null
+++ b/src/include/utils/skipsupport.h
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.h
+ *	  Support routines for B-Tree skip scan.
+ *
+ * B-Tree operator classes for discrete types can optionally provide a support
+ * function for skipping.  This is used during skip scans.
+ *
+ * A B-tree operator class that implements skip support provides B-tree index
+ * scans with a way of enumerating and iterating through every possible value
+ * from the domain of indexable values.  This gives scans a way to determine
+ * the next value in line for a given skip array/scan key/skipped attribute.
+ * Scans request the next (or previous) value whenever they run out of tuples
+ * matching the skip array's current element value.  The next (or previous)
+ * value can be used to relocate the scan; it is applied in combination with
+ * at least one additional lower-order non-skip key, taken from the query.
+ *
+ * Skip support is used by discrete type (e.g., integer and date) opclasses.
+ * Indexes with an attribute whose input opclass is of one of these types tend
+ * to store adjacent values in adjoining groups of index tuples.  Each time a
+ * skip scan with skip support successfully guesses that the next value in the
+ * index (for a given skipped column) is indeed the value that skip support
+ * just incremented its skip array to, it will have saved the scan some work.
+ * The scan will have avoided an index probe that directly finds the next
+ * value that appears in the index.  (When skip support guesses wrong, then it
+ * won't have saved any work, but it also won't have added any useless work.
+ * The failed attempt to locate exactly-matching index tuples acts just like
+ * an explicit probe would; it'll still find the index's true next value.)
+ *
+ * It usually isn't feasible to implement skip support for an opclass whose
+ * input type is continuous.  The B-Tree code falls back on next-key sentinel
+ * values for any opclass that doesn't provide its own skip support function.
+ * This isn't really an implementation restriction; there is no benefit to
+ * providing skip support for an opclass where guessing that the next indexed
+ * value is the next possible indexable value never (or hardly ever) works out.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/skipsupport.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SKIPSUPPORT_H
+#define SKIPSUPPORT_H
+
+#include "utils/relcache.h"
+
+typedef struct SkipSupportData *SkipSupport;
+typedef Datum (*SkipSupportIncDec) (Relation rel,
+									Datum existing,
+									bool *overflow);
+
+/*
+ * State/callbacks used by skip arrays to procedurally generate elements.
+ *
+ * A BTSKIPSUPPORT_PROC function must set each and every field when called
+ * (there are no optional fields).
+ */
+typedef struct SkipSupportData
+{
+	/*
+	 * low_elem and high_elem must be set with the lowest and highest possible
+	 * values from the domain of indexable values (assuming ascending order)
+	 */
+	Datum		low_elem;		/* lowest sorting/leftmost non-NULL value */
+	Datum		high_elem;		/* highest sorting/rightmost non-NULL value */
+
+	/*
+	 * Decrement/increment functions.
+	 *
+	 * Returns a decremented/incremented copy of caller's existing datum,
+	 * allocated in caller's memory context (for pass-by-reference types).
+	 * It's not okay for these functions to leak any memory.
+	 *
+	 * When the decrement function (or increment function) is called with a
+	 * value that already matches low_elem (or high_elem), function must set
+	 * the *overflow argument.  The return value is treated as undefined by
+	 * the B-Tree code; it shouldn't need to be (and won't be) pfree'd.
+	 *
+	 * The B-Tree code's "existing" datum argument is often just a straight
+	 * copy of a value from an index tuple.  Operator classes must accept
+	 * every possible representational variation within the underlying type.
+	 * On the other hand, opclasses are _not_ required to preserve information
+	 * that doesn't affect how datums are sorted (e.g., skip support for a
+	 * fixed precision numeric type needn't preserve datum display scale).
+	 * Operator class decrement/increment functions will never be called with
+	 * a NULL "existing" argument, either.
+	 */
+	SkipSupportIncDec decrement;
+	SkipSupportIncDec increment;
+} SkipSupportData;
+
+extern SkipSupport PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype,
+												 bool reverse);
+
+#endif							/* SKIPSUPPORT_H */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 55ec4c103..219df1971 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -489,7 +489,8 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	if (parallel_aware &&
 		indexRelation->rd_indam->amestimateparallelscan != NULL)
 		nbytes = add_size(nbytes,
-						  indexRelation->rd_indam->amestimateparallelscan(nkeys,
+						  indexRelation->rd_indam->amestimateparallelscan(indexRelation,
+																		  nkeys,
 																		  norderbys));
 
 	return nbytes;
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 291cb8fc1..4da5a3c1d 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -58,6 +58,7 @@
 #include <limits.h>
 
 #include "utils/fmgrprotos.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 #ifdef STRESS_SORT_INT_MIN
@@ -78,6 +79,51 @@ btboolcmp(PG_FUNCTION_ARGS)
 	PG_RETURN_INT32((int32) a - (int32) b);
 }
 
+static Datum
+bool_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == false)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return BoolGetDatum(bexisting - 1);
+}
+
+static Datum
+bool_increment(Relation rel, Datum existing, bool *overflow)
+{
+	bool		bexisting = DatumGetBool(existing);
+
+	if (bexisting == true)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return BoolGetDatum(bexisting + 1);
+}
+
+Datum
+btboolskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = bool_decrement;
+	sksup->increment = bool_increment;
+	sksup->low_elem = BoolGetDatum(false);
+	sksup->high_elem = BoolGetDatum(true);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint2cmp(PG_FUNCTION_ARGS)
 {
@@ -105,6 +151,51 @@ btint2sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int2_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int16GetDatum(iexisting - 1);
+}
+
+static Datum
+int2_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int16		iexisting = DatumGetInt16(existing);
+
+	if (iexisting == PG_INT16_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int16GetDatum(iexisting + 1);
+}
+
+Datum
+btint2skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int2_decrement;
+	sksup->increment = int2_increment;
+	sksup->low_elem = Int16GetDatum(PG_INT16_MIN);
+	sksup->high_elem = Int16GetDatum(PG_INT16_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint4cmp(PG_FUNCTION_ARGS)
 {
@@ -128,6 +219,51 @@ btint4sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int4_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int32GetDatum(iexisting - 1);
+}
+
+static Datum
+int4_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int32		iexisting = DatumGetInt32(existing);
+
+	if (iexisting == PG_INT32_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int32GetDatum(iexisting + 1);
+}
+
+Datum
+btint4skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int4_decrement;
+	sksup->increment = int4_increment;
+	sksup->low_elem = Int32GetDatum(PG_INT32_MIN);
+	sksup->high_elem = Int32GetDatum(PG_INT32_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint8cmp(PG_FUNCTION_ARGS)
 {
@@ -171,6 +307,51 @@ btint8sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+int8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return Int64GetDatum(iexisting - 1);
+}
+
+static Datum
+int8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	int64		iexisting = DatumGetInt64(existing);
+
+	if (iexisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return Int64GetDatum(iexisting + 1);
+}
+
+Datum
+btint8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = int8_decrement;
+	sksup->increment = int8_increment;
+	sksup->low_elem = Int64GetDatum(PG_INT64_MIN);
+	sksup->high_elem = Int64GetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btint48cmp(PG_FUNCTION_ARGS)
 {
@@ -292,6 +473,51 @@ btoidsortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+oid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == InvalidOid)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectIdGetDatum(oexisting - 1);
+}
+
+static Datum
+oid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid			oexisting = DatumGetObjectId(existing);
+
+	if (oexisting == OID_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectIdGetDatum(oexisting + 1);
+}
+
+Datum
+btoidskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid_decrement;
+	sksup->increment = oid_increment;
+	sksup->low_elem = ObjectIdGetDatum(InvalidOid);
+	sksup->high_elem = ObjectIdGetDatum(OID_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
@@ -325,3 +551,50 @@ btcharcmp(PG_FUNCTION_ARGS)
 	/* Be careful to compare chars as unsigned */
 	PG_RETURN_INT32((int32) ((uint8) a) - (int32) ((uint8) b));
 }
+
+static Datum
+char_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == 0)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return CharGetDatum((uint8) cexisting - 1);
+}
+
+static Datum
+char_increment(Relation rel, Datum existing, bool *overflow)
+{
+	uint8		cexisting = UInt8GetDatum(existing);
+
+	if (cexisting == UCHAR_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return CharGetDatum((uint8) cexisting + 1);
+}
+
+Datum
+btcharskipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = char_decrement;
+	sksup->increment = char_increment;
+
+	/* btcharcmp compares chars as unsigned */
+	sksup->low_elem = UInt8GetDatum(0);
+	sksup->high_elem = UInt8GetDatum(UCHAR_MAX);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 38a87af1c..5c08cda25 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -45,8 +45,15 @@ static bool _bt_compare_array_scankey_args(IndexScanDesc scan,
 										   ScanKey arraysk, ScanKey skey,
 										   FmgrInfo *orderproc, BTArrayKeyInfo *array,
 										   bool *qual_ok);
+static bool _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk,
+								 ScanKey skey, FmgrInfo *orderproc,
+								 BTArrayKeyInfo *array, bool *qual_ok);
+static bool _bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey,
+								 BTArrayKeyInfo *array, bool *qual_ok);
 static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys);
 static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap);
+static int	_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops,
+							   int *numSkipArrayKeys);
 static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
 									  Oid elemtype, StrategyNumber strat,
 									  Datum *elems, int nelems);
@@ -89,6 +96,8 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * within each attribute may be done as a byproduct of the processing here.
  * That process must leave array scan keys (within an attribute) in the same
  * order as corresponding entries from the scan's BTArrayKeyInfo array info.
+ * We might also construct skip array scan keys that weren't present in the
+ * original input keys; these are also output in standard attribute order.
  *
  * The output keys are marked with flags SK_BT_REQFWD and/or SK_BT_REQBKWD
  * if they must be satisfied in order to continue the scan forward or backward
@@ -101,10 +110,16 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * attributes with "=" keys are marked both SK_BT_REQFWD and SK_BT_REQBKWD.
  * For the first attribute without an "=" key, any "<" and "<=" keys are
  * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD.
- * This can be seen to be correct by considering the above example.  Note
- * in particular that if there are no keys for a given attribute, the keys for
- * subsequent attributes can never be required; for instance "WHERE y = 4"
- * requires a full-index scan.
+ * This can be seen to be correct by considering the above example.
+ *
+ * If we never generated skip array scan keys, it would be possible for "gaps"
+ * to appear that make it unsafe to mark any subsequent input scan keys
+ * (copied from scan->keyData[]) as required to continue the scan.  Prior to
+ * Postgres 18, a qual like "WHERE y = 4" always resulted in a full scan.
+ * This qual now becomes "WHERE x = ANY('{every possible x value}') and y = 4"
+ * on output.  In other words, preprocessing now adds a skip array on "x".
+ * This has the potential to be much more efficient than a full index scan
+ * (though it behaves like a full scan when there's many distinct "x" values).
  *
  * If possible, redundant keys are eliminated: we keep only the tightest
  * >/>= bound and the tightest </<= bound, and if there's an = key then
@@ -137,11 +152,21 @@ static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
  * Again, missing cross-type operators might cause us to fail to prove the
  * quals contradictory when they really are, but the scan will work correctly.
  *
- * Row comparison keys are currently also treated without any smarts:
- * we just transfer them into the preprocessed array without any
+ * Skip array = keys will even be generated in the presence of "contradictory"
+ * inequality quals when it'll enable marking later input quals as required.
+ * We'll merge any such inequalities into the generated skip array by setting
+ * its array.low_compare or array.high_compare key field.  The resulting skip
+ * array will generate its array elements from a range that's constrained by
+ * any merged input inequalities (which won't get output in so->keyData[]).
+ *
+ * Row comparison keys currently have a couple of notable limitations.
+ * Right now we just transfer them into the preprocessed array without any
  * editorialization.  We can treat them the same as an ordinary inequality
  * comparison on the row's first index column, for the purposes of the logic
- * about required keys.
+ * about required keys.  Also, we are unable to merge a row comparison key
+ * into a skip array (only ordinary inequalities are merged).  A key that
+ * comes after a Row comparison key is therefore never marked as required
+ * (we won't add a useless skip array that can't be merged with a RowCompare).
  *
  * Note: the reason we have to copy the preprocessed scan keys into private
  * storage is that we are modifying the array based on comparisons of the
@@ -200,6 +225,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
 		/* Also maintain keyDataMap for remapping so->orderProcs[] later */
 		keyDataMap = MemoryContextAlloc(so->arrayContext,
 										numberOfKeys * sizeof(int));
+
+		/*
+		 * Also enlarge output array when it might otherwise not have room for
+		 * a skip array's scan key
+		 */
+		if (numberOfKeys > scan->numberOfKeys)
+			so->keyData = repalloc(so->keyData,
+								   numberOfKeys * sizeof(ScanKeyData));
 	}
 	else
 		inkeys = scan->keyData;
@@ -229,6 +262,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			Assert(so->keyData[0].sk_flags & SK_SEARCHARRAY);
 			Assert(so->keyData[0].sk_strategy != BTEqualStrategyNumber ||
 				   (so->arrayKeys[0].scan_key == 0 &&
+					!(so->keyData[0].sk_flags & SK_BT_SKIP) &&
 					OidIsValid(so->orderProcs[0].fn_oid)));
 		}
 
@@ -288,7 +322,8 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * redundant.  Note that this is no less true if the = key is
 			 * SEARCHARRAY; the only real difference is that the inequality
 			 * key _becomes_ redundant by making _bt_compare_scankey_args
-			 * eliminate the subset of elements that won't need to be matched.
+			 * eliminate the subset of elements that won't need to be matched
+			 * (with SAOP arrays and skip arrays alike).
 			 *
 			 * If we have a case like "key = 1 AND key > 2", we set qual_ok to
 			 * false and abandon further processing.  We'll do the same thing
@@ -345,7 +380,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 							return;
 						}
 						/* else discard the redundant non-equality key */
-						Assert(!array || array->num_elems > 0);
 						xform[j].inkey = NULL;
 						xform[j].inkeyi = -1;
 					}
@@ -393,6 +427,11 @@ _bt_preprocess_keys(IndexScanDesc scan)
 			 * Emit the cleaned-up keys into the so->keyData[] array, and then
 			 * mark them if they are required.  They are required (possibly
 			 * only in one direction) if all attrs before this one had "=".
+			 *
+			 * In practice we'll rarely output non-required scan keys here;
+			 * typically, _bt_preprocess_array_keys has already added "=" keys
+			 * sufficient to form an unbroken series of "=" constraints on all
+			 * attrs prior to the attr from the final scan->keyData[] key.
 			 */
 			for (j = BTMaxStrategyNumber; --j >= 0;)
 			{
@@ -481,6 +520,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == i);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(inkey->sk_flags & SK_BT_SKIP));
 				}
 				else if (xform[j].inkey->sk_flags & SK_SEARCHARRAY)
 				{
@@ -489,6 +529,7 @@ _bt_preprocess_keys(IndexScanDesc scan)
 
 					Assert(array->scan_key == xform[j].inkeyi);
 					Assert(OidIsValid(orderproc->fn_oid));
+					Assert(!(xform[j].inkey->sk_flags & SK_BT_SKIP));
 				}
 
 				/*
@@ -508,8 +549,6 @@ _bt_preprocess_keys(IndexScanDesc scan)
 				/* Have all we need to determine redundancy */
 				if (test_result)
 				{
-					Assert(!array || array->num_elems > 0);
-
 					/*
 					 * New key is more restrictive, and so replaces old key...
 					 */
@@ -803,6 +842,9 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 				cmp_op;
 	StrategyNumber strat;
 
+	Assert(!((leftarg->sk_flags | rightarg->sk_flags) &
+			 (SK_ROW_HEADER | SK_ROW_MEMBER)));
+
 	/*
 	 * First, deal with cases where one or both args are NULL.  This should
 	 * only happen when the scankeys represent IS NULL/NOT NULL conditions.
@@ -812,6 +854,22 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		bool		leftnull,
 					rightnull;
 
+		/* Handle skip array comparison with IS NOT NULL scan key */
+		if ((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)
+		{
+			/* Shouldn't generate skip array in presence of IS NULL key */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNULL));
+			Assert((leftarg->sk_flags | rightarg->sk_flags) & SK_SEARCHNOTNULL);
+
+			/* Skip array will have no NULL element/IS NULL scan key */
+			Assert(array->num_elems == -1);
+			array->null_elem = false;
+
+			/* IS NOT NULL key (could be leftarg or rightarg) now redundant */
+			*result = true;
+			return true;
+		}
+
 		if (leftarg->sk_flags & SK_ISNULL)
 		{
 			Assert(leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
@@ -885,6 +943,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
 		{
 			/* Can't make the comparison */
 			*result = false;	/* suppress compiler warnings */
+			Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP));
 			return false;
 		}
 
@@ -978,24 +1037,55 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
  * Compare an array scan key to a scalar scan key, eliminating contradictory
  * array elements such that the scalar scan key becomes redundant.
  *
+ * If the opfamily is incomplete we may not be able to determine which
+ * elements are contradictory.  When we return true we'll have validly set
+ * *qual_ok, guaranteeing that at least the scalar scan key can be considered
+ * redundant.  We return false if the comparison could not be made (caller
+ * must keep both scan keys when this happens).
+ *
+ * Note: it's up to caller to deal with IS [NOT] NULL scan keys, as well as
+ * row comparison scan keys.  We only deal with scalar scan keys.
+ */
+static bool
+_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
+							   bool *qual_ok)
+{
+	Assert(arraysk->sk_attno == skey->sk_attno);
+	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
+		   arraysk->sk_strategy == BTEqualStrategyNumber);
+	/* don't expect to have to deal with NULLs/row comparison scan keys */
+	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
+	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
+		   skey->sk_strategy != BTEqualStrategyNumber);
+
+	/*
+	 * Just call the appropriate helper function based on whether it's a SAOP
+	 * array or a skip array.  Both helpers will set *qual_ok in passing.
+	 */
+	if (array->num_elems != -1)
+		return _bt_saoparray_shrink(scan, arraysk, skey, orderproc, array,
+									qual_ok);
+	else
+		return _bt_skiparray_shrink(scan, skey, array, qual_ok);
+}
+
+/*
+ * Preprocessing of SAOP (non-skip) array scan key, used to determine which
+ * array elements are eliminated as contradictory by a non-array scalar key.
+ * _bt_compare_array_scankey_args helper function.
+ *
  * Array elements can be eliminated as contradictory when excluded by some
  * other operator on the same attribute.  For example, with an index scan qual
  * "WHERE a IN (1, 2, 3) AND a < 2", all array elements except the value "1"
  * are eliminated, and the < scan key is eliminated as redundant.  Cases where
  * every array element is eliminated by a redundant scalar scan key have an
  * unsatisfiable qual, which we handle by setting *qual_ok=false for caller.
- *
- * If the opfamily doesn't supply a complete set of cross-type ORDER procs we
- * may not be able to determine which elements are contradictory.  If we have
- * the required ORDER proc then we return true (and validly set *qual_ok),
- * guaranteeing that at least the scalar scan key can be considered redundant.
- * We return false if the comparison could not be made (caller must keep both
- * scan keys when this happens).
  */
 static bool
-_bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
-							   FmgrInfo *orderproc, BTArrayKeyInfo *array,
-							   bool *qual_ok)
+_bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey,
+					 FmgrInfo *orderproc, BTArrayKeyInfo *array, bool *qual_ok)
 {
 	Relation	rel = scan->indexRelation;
 	Oid			opcintype = rel->rd_opcintype[arraysk->sk_attno - 1];
@@ -1006,14 +1096,8 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	FmgrInfo	crosstypeproc;
 	FmgrInfo   *orderprocp = orderproc;
 
-	Assert(arraysk->sk_attno == skey->sk_attno);
 	Assert(array->num_elems > 0);
-	Assert(!(arraysk->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert((arraysk->sk_flags & SK_SEARCHARRAY) &&
-		   arraysk->sk_strategy == BTEqualStrategyNumber);
-	Assert(!(skey->sk_flags & (SK_ISNULL | SK_ROW_HEADER | SK_ROW_MEMBER)));
-	Assert(!(skey->sk_flags & SK_SEARCHARRAY) ||
-		   skey->sk_strategy != BTEqualStrategyNumber);
+	Assert(!(arraysk->sk_flags & SK_BT_SKIP));
 
 	/*
 	 * _bt_binsrch_array_skey searches an array for the entry best matching a
@@ -1112,6 +1196,105 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
 	return true;
 }
 
+/*
+ * Preprocessing of skip (non-SAOP) array scan key, used to determine
+ * redundancy against a non-array scalar scan key (must be an inequality).
+ * _bt_compare_array_scankey_args helper function.
+ *
+ * Unlike _bt_saoparray_shrink, we don't modify caller's array in-place.  Skip
+ * arrays work by procedurally generating their elements as needed, so we just
+ * store the inequality as the skip array's low_compare or high_compare.  The
+ * array's elements will be generated from the range of values that satisfies
+ * both low_compare and high_compare.
+ */
+static bool
+_bt_skiparray_shrink(IndexScanDesc scan, ScanKey skey, BTArrayKeyInfo *array,
+					 bool *qual_ok)
+{
+	bool		test_result;
+
+	Assert(array->num_elems == -1);
+
+	/*
+	 * Array's index attribute will be constrained by a strict operator/key.
+	 * Array must not "contain a NULL element" (i.e. the scan must not apply
+	 * "IS NULL" qual when it reaches the end of the index that stores NULLs).
+	 */
+	array->null_elem = false;
+	*qual_ok = true;
+
+	/*
+	 * Consider if we should treat caller's scalar scan key as the skip
+	 * array's high_compare or low_compare.
+	 *
+	 * In general the current array element must either be a copy of a value
+	 * taken from an index tuple, or a derivative value generated by opclass's
+	 * skip support function.  That way the scan can always safely assume that
+	 * it's okay to use the input-opclass-only-type proc from so->orderProcs[]
+	 * (they can be cross-type with SAOP arrays, but never with skip arrays).
+	 *
+	 * This approach is enabled by MINVAL/MAXVAL sentinel key markings, which
+	 * can be thought of as representing either the lowest or highest matching
+	 * array element (excluding the NULL element, where applicable, though as
+	 * just discussed it isn't applicable to this range skip array anyway).
+	 * Array keys marked MINVAL/MAXVAL never have a valid datum in their
+	 * sk_argument field.  The scan directly applies the array's low_compare
+	 * key when it encounters MINVAL in the array key proper (just as it
+	 * applies high_compare when it sees MAXVAL set in the array key proper).
+	 * The scan must never use the array's so->orderProcs[] proc against
+	 * low_compare's/high_compare's sk_argument, either (so->orderProcs[] is
+	 * only intended to be used with rhs datums from the array proper/index).
+	 */
+	switch (skey->sk_strategy)
+	{
+		case BTLessStrategyNumber:
+		case BTLessEqualStrategyNumber:
+			if (array->high_compare)
+			{
+				/* replace existing high_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->high_compare, skey,
+											  array->high_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing high_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's high_compare */
+			array->high_compare = skey;
+			break;
+		case BTGreaterEqualStrategyNumber:
+		case BTGreaterStrategyNumber:
+			if (array->low_compare)
+			{
+				/* replace existing low_compare with caller's key? */
+				if (!_bt_compare_scankey_args(scan, array->low_compare, skey,
+											  array->low_compare, NULL, NULL,
+											  &test_result))
+					return false;	/* can't determine more restrictive key */
+
+				if (!test_result)
+					return true;	/* no, just discard caller's key */
+
+				/* yes, replace existing low_compare with caller's key */
+			}
+
+			/* caller's key becomes skip array's low_compare */
+			array->low_compare = skey;
+			break;
+		case BTEqualStrategyNumber:
+		default:
+			elog(ERROR, "unrecognized StrategyNumber: %d",
+				 (int) skey->sk_strategy);
+			break;
+	}
+
+	return true;
+}
+
 /*
  *	_bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
  *
@@ -1137,6 +1320,12 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * one equality strategy array scan key per index attribute.  We'll always be
  * able to set things up that way when complete opfamilies are used.
  *
+ * We're also responsible for generating skip arrays (and their associated
+ * scan keys) here.  This enables skip scan.  We do this for index attributes
+ * that initially lacked an equality condition within scan->keyData[], iff
+ * doing so allows a later scan key (that was passed to us in scan->keyData[])
+ * to be marked required by our _bt_preprocess_keys caller.
+ *
  * We set the scan key references from the scan's BTArrayKeyInfo info array to
  * offsets into the temp modified input array returned to caller.  Scans that
  * have array keys should call _bt_preprocess_array_keys_final when standard
@@ -1144,49 +1333,45 @@ _bt_compare_array_scankey_args(IndexScanDesc scan, ScanKey arraysk, ScanKey skey
  * references into references to the scan's so->keyData[] output scan keys.
  *
  * Note: the reason we need to return a temp scan key array, rather than just
- * scribbling on scan->keyData, is that callers are permitted to call btrescan
- * without supplying a new set of scankey data.
+ * modifying scan->keyData[], is that callers are permitted to call btrescan
+ * without supplying a new set of scankey data.  Certain other preprocessing
+ * routines (e.g., _bt_fix_scankey_strategy) _can_ modify scan->keyData[], but
+ * we can't make that work here because our modifications are non-idempotent.
  */
 static ScanKey
 _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
-	int			numberOfKeys = scan->numberOfKeys;
 	int16	   *indoption = rel->rd_indoption;
+	Oid			skip_eq_ops[INDEX_MAX_KEYS];
 	int			numArrayKeys,
-				output_ikey = 0;
+				numSkipArrayKeys,
+				numArrayKeyData;
+	AttrNumber	attno_skip = 1;
 	int			origarrayatt = InvalidAttrNumber,
 				origarraykey = -1;
 	Oid			origelemtype = InvalidOid;
-	ScanKey		cur;
 	MemoryContext oldContext;
 	ScanKey		arrayKeyData;	/* modified copy of scan->keyData */
 
-	Assert(numberOfKeys);
-
-	/* Quick check to see if there are any array keys */
-	numArrayKeys = 0;
-	for (int i = 0; i < numberOfKeys; i++)
-	{
-		cur = &scan->keyData[i];
-		if (cur->sk_flags & SK_SEARCHARRAY)
-		{
-			numArrayKeys++;
-			Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
-			/* If any arrays are null as a whole, we can quit right now. */
-			if (cur->sk_flags & SK_ISNULL)
-			{
-				so->qual_ok = false;
-				return NULL;
-			}
-		}
-	}
+	/*
+	 * Check the number of input array keys within scan->keyData[] input keys
+	 * (also checks if we should add extra skip arrays based on input keys)
+	 */
+	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
 		return NULL;
 
+	/*
+	 * Estimated final size of arrayKeyData[] array we'll return to our caller
+	 * is the size of the original scan->keyData[] input array, plus space for
+	 * any additional skip array scan keys we'll need to generate below
+	 */
+	numArrayKeyData = scan->numberOfKeys + numSkipArrayKeys;
+
 	/*
 	 * Make a scan-lifespan context to hold array-associated data, or reset it
 	 * if we already have one from a previous rescan cycle.
@@ -1201,18 +1386,20 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	oldContext = MemoryContextSwitchTo(so->arrayContext);
 
 	/* Create output scan keys in the workspace context */
-	arrayKeyData = (ScanKey) palloc(numberOfKeys * sizeof(ScanKeyData));
+	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-	so->orderProcs = (FmgrInfo *) palloc(numberOfKeys * sizeof(FmgrInfo));
+	so->orderProcs = (FmgrInfo *) palloc(numArrayKeyData * sizeof(FmgrInfo));
 
-	/* Now process each array key */
 	numArrayKeys = 0;
-	for (int input_ikey = 0; input_ikey < numberOfKeys; input_ikey++)
+	numArrayKeyData = 0;
+	for (int input_ikey = 0; input_ikey < scan->numberOfKeys; input_ikey++)
 	{
+		ScanKey		inkey = scan->keyData + input_ikey,
+					cur;
 		FmgrInfo	sortproc;
 		FmgrInfo   *sortprocp = &sortproc;
 		Oid			elemtype;
@@ -1225,21 +1412,113 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		Datum	   *elem_values;
 		bool	   *elem_nulls;
 		int			num_nonnulls;
-		int			j;
+
+		/* set up next output scan key */
+		cur = &arrayKeyData[numArrayKeyData];
+
+		/* Backfill skip arrays for attrs < or <= input key's attr? */
+		while (numSkipArrayKeys && attno_skip <= inkey->sk_attno)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+			Oid			collation = rel->rd_indcollation[attno_skip - 1];
+			Oid			eq_op = skip_eq_ops[attno_skip - 1];
+			CompactAttribute *attr;
+			RegProcedure cmp_proc;
+
+			if (!OidIsValid(eq_op))
+			{
+				/*
+				 * Attribute already has an = input key, so don't output a
+				 * skip array for attno_skip.  Just copy attribute's = input
+				 * key into arrayKeyData[] once outside this inner loop.
+				 *
+				 * Note: When we get here there must be a later attribute that
+				 * lacks an equality input key, and still needs a skip array
+				 * (if there wasn't then numSkipArrayKeys would be 0 by now).
+				 */
+				Assert(attno_skip == inkey->sk_attno);
+				/* inkey can't be last input key to be marked required: */
+				Assert(input_ikey < scan->numberOfKeys - 1);
+#if 0
+				/* Could be a redundant input scan key, so can't do this: */
+				Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+					   (inkey->sk_flags & SK_SEARCHNULL));
+#endif
+
+				attno_skip++;
+				break;
+			}
+
+			cmp_proc = get_opcode(eq_op);
+			if (!RegProcedureIsValid(cmp_proc))
+				elog(ERROR, "missing oprcode for skipping equals operator %u", eq_op);
+
+			ScanKeyEntryInitialize(cur,
+								   SK_SEARCHARRAY | SK_BT_SKIP, /* flags */
+								   attno_skip,	/* skipped att number */
+								   BTEqualStrategyNumber,	/* equality strategy */
+								   InvalidOid,	/* opclass input subtype */
+								   collation,	/* index column's collation */
+								   cmp_proc,	/* equality operator's proc */
+								   (Datum) 0);	/* constant */
+
+			/* Initialize generic BTArrayKeyInfo fields */
+			so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
+			so->arrayKeys[numArrayKeys].num_elems = -1;
+
+			/* Initialize skip array specific BTArrayKeyInfo fields */
+			attr = TupleDescCompactAttr(RelationGetDescr(rel), attno_skip - 1);
+			reverse = (indoption[attno_skip - 1] & INDOPTION_DESC) != 0;
+			so->arrayKeys[numArrayKeys].attlen = attr->attlen;
+			so->arrayKeys[numArrayKeys].attbyval = attr->attbyval;
+			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
+			so->arrayKeys[numArrayKeys].sksup =
+				PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse);
+			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
+			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
+
+			/*
+			 * We'll need a 3-way ORDER proc.  Set that up now.
+			 */
+			_bt_setup_array_cmp(scan, cur, opcintype,
+								&so->orderProcs[numArrayKeyData], NULL);
+
+			numArrayKeys++;
+			numArrayKeyData++;	/* keep this scan key/array */
+
+			/* set up next output scan key */
+			cur = &arrayKeyData[numArrayKeyData];
+
+			/* remember having output this skip array and scan key */
+			numSkipArrayKeys--;
+			attno_skip++;
+		}
 
 		/*
 		 * Provisionally copy scan key into arrayKeyData[] array we'll return
 		 * to _bt_preprocess_keys caller
 		 */
-		cur = &arrayKeyData[output_ikey];
-		*cur = scan->keyData[input_ikey];
+		*cur = *inkey;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY))
 		{
-			output_ikey++;		/* keep this non-array scan key */
+			numArrayKeyData++;	/* keep this non-array scan key */
 			continue;
 		}
 
+		/*
+		 * Process SAOP array scan key
+		 */
+		Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
+
+		/* If array is null as a whole, the scan qual is unsatisfiable */
+		if (cur->sk_flags & SK_ISNULL)
+		{
+			so->qual_ok = false;
+			break;
+		}
+
 		/*
 		 * Deconstruct the array into elements
 		 */
@@ -1257,7 +1536,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * all btree operators are strict.
 		 */
 		num_nonnulls = 0;
-		for (j = 0; j < num_elems; j++)
+		for (int j = 0; j < num_elems; j++)
 		{
 			if (!elem_nulls[j])
 				elem_values[num_nonnulls++] = elem_values[j];
@@ -1295,7 +1574,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTGreaterStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			case BTEqualStrategyNumber:
 				/* proceed with rest of loop */
@@ -1306,7 +1585,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 					_bt_find_extreme_element(scan, cur, elemtype,
 											 BTLessStrategyNumber,
 											 elem_values, num_nonnulls);
-				output_ikey++;	/* keep this transformed scan key */
+				numArrayKeyData++;	/* keep this transformed scan key */
 				continue;
 			default:
 				elog(ERROR, "unrecognized StrategyNumber: %d",
@@ -1323,7 +1602,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		 * sortproc just points to the same proc used during binary searches.
 		 */
 		_bt_setup_array_cmp(scan, cur, elemtype,
-							&so->orderProcs[output_ikey], &sortprocp);
+							&so->orderProcs[numArrayKeyData], &sortprocp);
 
 		/*
 		 * Sort the non-null elements and eliminate any duplicates.  We must
@@ -1392,23 +1671,24 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			origelemtype = elemtype;
 		}
 
-		/*
-		 * And set up the BTArrayKeyInfo data.
-		 *
-		 * Note: _bt_preprocess_array_keys_final will fix-up each array's
-		 * scan_key field later on, after so->keyData[] has been finalized.
-		 */
-		so->arrayKeys[numArrayKeys].scan_key = output_ikey;
+		/* Initialize generic BTArrayKeyInfo fields */
+		so->arrayKeys[numArrayKeys].scan_key = numArrayKeyData;
 		so->arrayKeys[numArrayKeys].num_elems = num_elems;
+
+		/* Initialize SAOP array specific BTArrayKeyInfo fields */
 		so->arrayKeys[numArrayKeys].elem_values = elem_values;
+		so->arrayKeys[numArrayKeys].cur_elem = -1;	/* i.e. invalid */
+
 		numArrayKeys++;
-		output_ikey++;			/* keep this scan key/array */
+		numArrayKeyData++;		/* keep this scan key/array */
 	}
 
+	Assert(numSkipArrayKeys == 0);
+
 	/* Set final number of equality-type array keys */
 	so->numArrayKeys = numArrayKeys;
-	/* Set number of scan keys remaining in arrayKeyData[] */
-	*new_numberOfKeys = output_ikey;
+	/* Set number of scan keys in arrayKeyData[] */
+	*new_numberOfKeys = numArrayKeyData;
 
 	MemoryContextSwitchTo(oldContext);
 
@@ -1514,7 +1794,14 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 		{
 			BTArrayKeyInfo *array = &so->arrayKeys[arrayidx];
 
-			Assert(array->num_elems > 0);
+			/*
+			 * All skip arrays must be marked required, and final column can
+			 * never have a skip array
+			 */
+			Assert(array->num_elems > 0 || array->num_elems == -1);
+			Assert(array->num_elems != -1 || outkey->sk_flags & SK_BT_REQFWD);
+			Assert(array->num_elems != -1 ||
+				   outkey->sk_attno < IndexRelationGetNumberOfKeyAttributes(rel));
 
 			if (array->scan_key == input_ikey)
 			{
@@ -1575,6 +1862,197 @@ _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap)
 								 so->numArrayKeys, INDEX_MAX_KEYS)));
 }
 
+/*
+ *	_bt_num_array_keys() -- determine # of BTArrayKeyInfo entries
+ *
+ * _bt_preprocess_array_keys helper function.  Returns the estimated size of
+ * the scan's BTArrayKeyInfo array, which is guaranteed to be large enough to
+ * fit every so->arrayKeys[] entry.
+ *
+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller must add this many
+ * skip arrays to each of the most significant attributes lacking any keys
+ * that use the = strategy (IS NULL keys count as = keys here).  The specific
+ * attributes that need skip arrays are indicated by initializing caller's
+ * skip_eq_ops[] 0-based attribute offset to a valid = op strategy Oid.  We'll
+ * only ever set skip_eq_ops[] entries to InvalidOid for attributes that
+ * already have an equality key in scan->keyData[] input keys -- and only when
+ * there's some later "attribute gap" for us to "fill-in" with a skip array.
+ *
+ * We're optimistic about skipping working out: we always add exactly the skip
+ * arrays needed to maximize the number of input scan keys that can ultimately
+ * be marked as required to continue the scan (but no more).  For a composite
+ * index on (a, b, c, d), we'll instruct caller to add skip arrays as follows:
+ *
+ * Input keys						Output keys (after all preprocessing)
+ * ----------						-------------------------------------
+ * a = 1							a = 1 (no skip arrays)
+ * b = 42							skip a AND b = 42
+ * a = 1 AND b = 42					a = 1 AND b = 42 (no skip arrays)
+ * a >= 1 AND b = 42				range skip a AND b = 42
+ * a = 1 AND b > 42					a = 1 AND b > 42 (no skip arrays)
+ * a >= 1 AND a <= 3 AND b = 42		range skip a AND b = 42
+ * a = 1 AND c <= 27				a = 1 AND skip b AND c <= 27
+ * a = 1 AND d >= 1					a = 1 AND skip b AND skip c AND d >= 1
+ * a = 1 AND b >= 42 AND d > 1		a = 1 AND range skip b AND skip c AND d > 1
+ */
+static int
+_bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops, int *numSkipArrayKeys)
+{
+	Relation	rel = scan->indexRelation;
+	AttrNumber	attno_skip = 1,
+				attno_inkey = 1;
+	bool		attno_has_equal = false,
+				attno_has_rowcompare = false;
+	int			numSAOPArrayKeys,
+				prev_numSkipArrayKeys;
+
+	Assert(scan->numberOfKeys);
+
+	/* Initial pass over input scan keys counts the number of SAOP arrays */
+	numSAOPArrayKeys = 0;
+	prev_numSkipArrayKeys = 0;
+	*numSkipArrayKeys = 0;
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		if (inkey->sk_flags & SK_SEARCHARRAY)
+			numSAOPArrayKeys++;
+	}
+
+#ifdef DEBUG_DISABLE_SKIP_SCAN
+	/* don't attempt to add skip arrays */
+	return numArrayKeys;
+#endif
+
+	for (int i = 0;; i++)
+	{
+		ScanKey		inkey = scan->keyData + i;
+
+		/*
+		 * Backfill skip arrays for any wholly omitted attributes prior to
+		 * attno_inkey
+		 */
+		while (attno_skip < attno_inkey)
+		{
+			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+			/* Look up input opclass's equality operator (might fail) */
+			skip_eq_ops[attno_skip - 1] =
+				get_opfamily_member(opfamily, opcintype, opcintype,
+									BTEqualStrategyNumber);
+			if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+			{
+				/*
+				 * Cannot generate a skip array for this or later attributes
+				 * (input opclass lacks an equality strategy operator)
+				 */
+				*numSkipArrayKeys = prev_numSkipArrayKeys;
+				return numSAOPArrayKeys + prev_numSkipArrayKeys;
+			}
+
+			/* plan on adding a backfill skip array for this attribute */
+			(*numSkipArrayKeys)++;
+			attno_skip++;
+		}
+
+		prev_numSkipArrayKeys = *numSkipArrayKeys;
+
+		/*
+		 * Stop once past the final input scan key.  We deliberately never add
+		 * a skip array for the last input scan key's attribute -- even when
+		 * there are only inequality keys on that attribute.
+		 */
+		if (i == scan->numberOfKeys)
+			break;
+
+		/*
+		 * Later preprocessing steps cannot merge a RowCompare into a skip
+		 * array, so stop adding skip arrays once we see one.  (Note that we
+		 * can backfill skip arrays before a RowCompare, which will allow keys
+		 * up to and including the RowCompare to be marked required.)
+		 *
+		 * Skip arrays work by maintaining a current array element value,
+		 * which anchors lower-order keys via an implied equality constraint.
+		 * This is incompatible with the current nbtree row comparison design,
+		 * which compares all columns together, as an indivisible group.
+		 * Alternative designs that can be used alongside skip arrays are
+		 * possible, but it's not clear that they're really worth pursuing.
+		 *
+		 * A RowCompare qual "(a, b, c) > (10, 'foo', 42)" is equivalent to
+		 * "((a=10 AND b='foo' AND c>42) OR (a=10 AND b>'foo') OR (a>10))".
+		 * Such a RowCompare can be decomposed into 3 disjuncts, each of which
+		 * can be executed as a separate "single value" index scan.  That'd
+		 * give all 3 scans the ability to add skip arrays in the usual way
+		 * (when there are any scalar low-order keys after the RowCompare).
+		 * Under this scheme, a qual "(a, b, c) > (10, 'foo', 42) AND d = 99"
+		 * performs 3 separate scans, each of which can mark keys up to and
+		 * including its "d = 99" key as required to continue the scan.
+		 */
+		if (attno_has_rowcompare)
+			break;
+
+		/*
+		 * Now consider next attno_inkey (or keep going if this is an
+		 * additional scan key against the same attribute)
+		 */
+		if (attno_inkey < inkey->sk_attno)
+		{
+			/*
+			 * Now add skip array for previous scan key's attribute, though
+			 * only if the attribute has no equality strategy scan keys
+			 */
+			if (attno_has_equal)
+			{
+				/* Attributes with an = key must have InvalidOid eq_op set */
+				skip_eq_ops[attno_skip - 1] = InvalidOid;
+			}
+			else
+			{
+				Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
+				Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
+
+				/* Look up input opclass's equality operator (might fail) */
+				skip_eq_ops[attno_skip - 1] =
+					get_opfamily_member(opfamily, opcintype, opcintype,
+										BTEqualStrategyNumber);
+
+				if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+				{
+					/*
+					 * Input opclass lacks an equality strategy operator, so
+					 * don't generate a skip array that definitely won't work
+					 */
+					break;
+				}
+
+				/* plan on adding a backfill skip array for this attribute */
+				(*numSkipArrayKeys)++;
+			}
+
+			/* Set things up for this new attribute */
+			attno_skip++;
+			attno_inkey = inkey->sk_attno;
+			attno_has_equal = false;
+		}
+
+		/*
+		 * Track if this attribute's scan keys include any equality strategy
+		 * scan keys (IS NULL keys count as equality keys here).  Also track
+		 * if it has any RowCompare keys.
+		 */
+		if (inkey->sk_strategy == BTEqualStrategyNumber ||
+			(inkey->sk_flags & SK_SEARCHNULL))
+			attno_has_equal = true;
+		if (inkey->sk_flags & SK_ROW_HEADER)
+			attno_has_rowcompare = true;
+	}
+
+	return numSAOPArrayKeys + *numSkipArrayKeys;
+}
+
 /*
  * _bt_find_extreme_element() -- get least or greatest array element
  *
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 4a0bf069f..bdadbf73c 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -31,6 +31,7 @@
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
 #include "storage/read_stream.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
 #include "utils/memutils.h"
@@ -76,14 +77,26 @@ typedef struct BTParallelScanDescData
 
 	/*
 	 * btps_arrElems is used when scans need to schedule another primitive
-	 * index scan.  Holds BTArrayKeyInfo.cur_elem offsets for scan keys.
+	 * index scan with one or more SAOP arrays.  Holds BTArrayKeyInfo.cur_elem
+	 * offsets for each = scan key associated with a ScalarArrayOp array.
 	 */
 	int			btps_arrElems[FLEXIBLE_ARRAY_MEMBER];
+
+	/*
+	 * Additional space (at the end of the struct) is used when scans need to
+	 * schedule another primitive index scan with one or more skip arrays.
+	 * Holds a flattened datum representation for each = scan key associated
+	 * with a skip array.
+	 */
 }			BTParallelScanDescData;
 
 typedef struct BTParallelScanDescData *BTParallelScanDesc;
 
 
+static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+										  BTScanOpaque so);
+static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+										BTScanOpaque so);
 static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 						 IndexBulkDeleteCallback callback, void *callback_state,
 						 BTCycleId cycleid);
@@ -541,10 +554,167 @@ btrestrpos(IndexScanDesc scan)
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
 Size
-btestimateparallelscan(int nkeys, int norderbys)
+btestimateparallelscan(Relation rel, int nkeys, int norderbys)
 {
-	/* Pessimistically assume all input scankeys will be output with arrays */
-	return offsetof(BTParallelScanDescData, btps_arrElems) + sizeof(int) * nkeys;
+	int16		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	Size		estnbtreeshared,
+				genericattrspace;
+
+	/*
+	 * Pessimistically assume that every input scan key will be output with
+	 * its own SAOP array
+	 */
+	estnbtreeshared = offsetof(BTParallelScanDescData, btps_arrElems) +
+		sizeof(int) * nkeys;
+
+	/* Single column indexes cannot possibly use a skip array */
+	if (nkeyatts == 1)
+		return estnbtreeshared;
+
+	/*
+	 * Pessimistically assume that all attributes prior to the least
+	 * significant attribute require a skip array (and an associated key)
+	 */
+	genericattrspace = datumEstimateSpace((Datum) 0, false, true,
+										  sizeof(Datum));
+	for (int attnum = 1; attnum < nkeyatts; attnum++)
+	{
+		CompactAttribute *attr;
+
+		/*
+		 * We make the conservative assumption that every index column will
+		 * also require a skip array.
+		 *
+		 * Every skip array must have space to store its scan key's sk_flags.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, sizeof(int));
+
+		/* Consider space required to store a datum of opclass input type */
+		attr = TupleDescCompactAttr(rel->rd_att, attnum - 1);
+		if (attr->attbyval)
+		{
+			/* This index attribute stores pass-by-value datums */
+			Size		estfixed = datumEstimateSpace((Datum) 0, false,
+													  true, attr->attlen);
+
+			estnbtreeshared = add_size(estnbtreeshared, estfixed);
+			continue;
+		}
+
+		/*
+		 * This index attribute stores pass-by-reference datums.
+		 *
+		 * Assume that serializing this array will use just as much space as a
+		 * pass-by-value datum, in addition to space for the largest possible
+		 * whole index tuple (this is not just a per-datum portion of the
+		 * largest possible tuple because that'd be almost as large anyway).
+		 *
+		 * This is quite conservative, but it's not clear how we could do much
+		 * better.  The executor requires an up-front storage request size
+		 * that reliably covers the scan's high watermark memory usage.  We
+		 * can't be sure of the real high watermark until the scan is over.
+		 */
+		estnbtreeshared = add_size(estnbtreeshared, genericattrspace);
+		estnbtreeshared = add_size(estnbtreeshared, BTMaxItemSize);
+	}
+
+	return estnbtreeshared;
+}
+
+/*
+ * _bt_parallel_serialize_arrays() -- Serialize parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan,
+							  BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+
+		if (array->num_elems != -1)
+		{
+			/* Save SAOP array's cur_elem (no need to copy key/datum) */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			btscan->btps_arrElems[i] = array->cur_elem;
+			continue;
+		}
+
+		/* Save all mutable state associated with skip array's key */
+		Assert(skey->sk_flags & SK_BT_SKIP);
+		memcpy(datumshared, &skey->sk_flags, sizeof(int));
+		datumshared += sizeof(int);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to serialize */
+			Assert(skey->sk_argument == 0);
+			continue;
+		}
+
+		datumSerialize(skey->sk_argument, (skey->sk_flags & SK_ISNULL) != 0,
+					   array->attbyval, array->attlen, &datumshared);
+	}
+}
+
+/*
+ * _bt_parallel_restore_arrays() -- Restore serialized parallel array state.
+ *
+ * Caller must have exclusively locked btscan->btps_lock when called.
+ */
+static void
+_bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan,
+							BTScanOpaque so)
+{
+	char	   *datumshared;
+
+	/* Space for serialized datums begins immediately after btps_arrElems[] */
+	datumshared = ((char *) &btscan->btps_arrElems[so->numArrayKeys]);
+	for (int i = 0; i < so->numArrayKeys; i++)
+	{
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
+		bool		isnull;
+
+		if (array->num_elems != -1)
+		{
+			/* Restore SAOP array using its saved cur_elem */
+			Assert(!(skey->sk_flags & SK_BT_SKIP));
+			array->cur_elem = btscan->btps_arrElems[i];
+			skey->sk_argument = array->elem_values[array->cur_elem];
+			continue;
+		}
+
+		/* Restore skip array by restoring its key directly */
+		if (!array->attbyval && skey->sk_argument)
+			pfree(DatumGetPointer(skey->sk_argument));
+		skey->sk_argument = (Datum) 0;
+		memcpy(&skey->sk_flags, datumshared, sizeof(int));
+		datumshared += sizeof(int);
+
+		Assert(skey->sk_flags & SK_BT_SKIP);
+
+		if (skey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))
+		{
+			/* No sk_argument datum to restore */
+			continue;
+		}
+
+		skey->sk_argument = datumRestore(&datumshared, &isnull);
+		if (isnull)
+		{
+			Assert(skey->sk_argument == 0);
+			Assert(skey->sk_flags & SK_SEARCHNULL);
+			Assert(skey->sk_flags & SK_ISNULL);
+		}
+	}
 }
 
 /*
@@ -613,6 +783,7 @@ bool
 _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 				   BlockNumber *last_curr_page, bool first)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	bool		exit_loop = false,
 				status = true,
@@ -679,14 +850,9 @@ _bt_parallel_seize(IndexScanDesc scan, BlockNumber *next_scan_page,
 			{
 				/* Can start scheduled primitive scan right away, so do so */
 				btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
-				for (int i = 0; i < so->numArrayKeys; i++)
-				{
-					BTArrayKeyInfo *array = &so->arrayKeys[i];
-					ScanKey		skey = &so->keyData[array->scan_key];
 
-					array->cur_elem = btscan->btps_arrElems[i];
-					skey->sk_argument = array->elem_values[array->cur_elem];
-				}
+				/* Restore scan's array keys from serialized values */
+				_bt_parallel_restore_arrays(rel, btscan, so);
 				exit_loop = true;
 			}
 			else
@@ -831,6 +997,7 @@ _bt_parallel_done(IndexScanDesc scan)
 void
 _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ParallelIndexScanDesc parallel_scan = scan->parallel_scan;
 	BTParallelScanDesc btscan;
@@ -849,12 +1016,7 @@ _bt_parallel_primscan_schedule(IndexScanDesc scan, BlockNumber curr_page)
 		btscan->btps_pageStatus = BTPARALLEL_NEED_PRIMSCAN;
 
 		/* Serialize scan's current array keys */
-		for (int i = 0; i < so->numArrayKeys; i++)
-		{
-			BTArrayKeyInfo *array = &so->arrayKeys[i];
-
-			btscan->btps_arrElems[i] = array->cur_elem;
-		}
+		_bt_parallel_serialize_arrays(rel, btscan, so);
 	}
 	LWLockRelease(&btscan->btps_lock);
 }
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 3d46fb5df..1ef2cb2b5 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -983,7 +983,21 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	 * one we use --- by definition, they are either redundant or
 	 * contradictory.
 	 *
-	 * Any regular (not SK_SEARCHNULL) key implies a NOT NULL qualifier.
+	 * In practice we rarely see any "attribute boundary key gaps" here.
+	 * Preprocessing can usually backfill skip array keys for any attributes
+	 * that were omitted from the original scan->keyData[] input keys.  All
+	 * array keys are always considered = keys, but we'll sometimes need to
+	 * treat the current key value as if we were using an inequality strategy.
+	 * This happens with range skip arrays, which store inequality keys in the
+	 * array's low_compare/high_compare fields (used to find the first/last
+	 * set of matches, when = key will lack a usable sk_argument value).
+	 * These are always preferred over any redundant "standard" inequality
+	 * keys on the same column (per the usual rule about preferring = keys).
+	 * Note also that any column with an = skip array key can never have an
+	 * additional, contradictory = key.
+	 *
+	 * All keys (with the exception of SK_SEARCHNULL keys and SK_BT_SKIP
+	 * array keys whose array is "null_elem=true") imply a NOT NULL qualifier.
 	 * If the index stores nulls at the end of the index we'll be starting
 	 * from, and we have no boundary key for the column (which means the key
 	 * we deduced NOT NULL from is an inequality key that constrains the other
@@ -1040,8 +1054,54 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 			if (i >= so->numberOfKeys || cur->sk_attno != curattr)
 			{
 				/*
-				 * Done looking at keys for curattr.  If we didn't find a
-				 * usable boundary key, see if we can deduce a NOT NULL key.
+				 * Done looking at keys for curattr.
+				 *
+				 * If this is a scan key for a skip array whose current
+				 * element is MINVAL, choose low_compare (when scanning
+				 * backwards it'll be MAXVAL, and we'll choose high_compare).
+				 *
+				 * Note: if the array's low_compare key makes 'chosen' NULL,
+				 * then we behave as if the array's first element is -inf,
+				 * except when !array->null_elem implies a usable NOT NULL
+				 * constraint.
+				 */
+				if (chosen != NULL &&
+					(chosen->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))
+				{
+					int			ikey = chosen - so->keyData;
+					ScanKey		skipequalitykey = chosen;
+					BTArrayKeyInfo *array = NULL;
+
+					for (int arridx = 0; arridx < so->numArrayKeys; arridx++)
+					{
+						array = &so->arrayKeys[arridx];
+						if (array->scan_key == ikey)
+							break;
+					}
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MAXVAL));
+						chosen = array->low_compare;
+					}
+					else
+					{
+						Assert(!(skipequalitykey->sk_flags & SK_BT_MINVAL));
+						chosen = array->high_compare;
+					}
+
+					Assert(chosen == NULL ||
+						   chosen->sk_attno == skipequalitykey->sk_attno);
+
+					if (!array->null_elem)
+						impliesNN = skipequalitykey;
+					else
+						Assert(chosen == NULL && impliesNN == NULL);
+				}
+
+				/*
+				 * If we didn't find a usable boundary key, see if we can
+				 * deduce a NOT NULL key
 				 */
 				if (chosen == NULL && impliesNN != NULL &&
 					((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ?
@@ -1084,9 +1144,40 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 					break;
 
 				/*
-				 * Done if that was the last attribute, or if next key is not
-				 * in sequence (implying no boundary key is available for the
-				 * next attribute).
+				 * If the key that we just added to startKeys[] is a skip
+				 * array = key whose current element is marked NEXT or PRIOR,
+				 * make strat_total > or < (and stop adding boundary keys).
+				 * This can only happen with opclasses that lack skip support.
+				 */
+				if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR))
+				{
+					Assert(chosen->sk_flags & SK_BT_SKIP);
+					Assert(strat_total == BTEqualStrategyNumber);
+
+					if (ScanDirectionIsForward(dir))
+					{
+						Assert(!(chosen->sk_flags & SK_BT_PRIOR));
+						strat_total = BTGreaterStrategyNumber;
+					}
+					else
+					{
+						Assert(!(chosen->sk_flags & SK_BT_NEXT));
+						strat_total = BTLessStrategyNumber;
+					}
+
+					/*
+					 * We're done.  We'll never find an exact = match for a
+					 * NEXT or PRIOR sentinel sk_argument value.  There's no
+					 * sense in trying to add more keys to startKeys[].
+					 */
+					break;
+				}
+
+				/*
+				 * Done if that was the last scan key output by preprocessing.
+				 * Also done if there is a gap index attribute that lacks a
+				 * usable key (only possible when preprocessing was unable to
+				 * generate a skip array key to "fill in the gap").
 				 */
 				if (i >= so->numberOfKeys ||
 					cur->sk_attno != curattr + 1)
@@ -1581,31 +1672,10 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	 * We skip this for the first page read by each (primitive) scan, to avoid
 	 * slowing down point queries.  They typically don't stand to gain much
 	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.
-	 *
-	 * The optimization is unsafe and must be avoided whenever _bt_checkkeys
-	 * just set a low-order required array's key to the best available match
-	 * for a truncated -inf attribute value from the prior page's high key
-	 * (array element 0 is always the best available match in this scenario).
-	 * It's quite likely that matches for array element 0 begin on this page,
-	 * but the start of matches won't necessarily align with page boundaries.
-	 * When the start of matches is somewhere in the middle of this page, it
-	 * would be wrong to treat page's final non-pivot tuple as representative.
-	 * Doing so might lead us to treat some of the page's earlier tuples as
-	 * being part of a group of tuples thought to satisfy the required keys.
-	 *
-	 * Note: Conversely, in the case where the scan's arrays just advanced
-	 * using the prior page's HIKEY _without_ advancement setting scanBehind,
-	 * the start of matches must be aligned with page boundaries, which makes
-	 * it safe to attempt the optimization here now.  It's also safe when the
-	 * prior page's HIKEY simply didn't need to advance any required array. In
-	 * both cases we can safely assume that the _first_ tuple from this page
-	 * must be >= the current set of array keys/equality constraints. And so
-	 * if the final tuple is == those same keys (and also satisfies any
-	 * required < or <= strategy scan keys) during the precheck, we can safely
-	 * assume that this must also be true of all earlier tuples from the page.
+	 * overhead of the precheck.  Also avoid it during scans with array keys,
+	 * which might be using skip scan (XXX fixed in next commit).
 	 */
-	if (!pstate.firstpage && !so->scanBehind && minoff < maxoff)
+	if (!pstate.firstpage && !arrayKeys && minoff < maxoff)
 	{
 		ItemId		iid;
 		IndexTuple	itup;
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 2aee9bbf6..108030a8e 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -30,6 +30,17 @@
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
 										   Datum arrdatum, ScanKey cur);
+static void _bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+									   Datum tupdatum, bool tupnull,
+									   BTArrayKeyInfo *array, ScanKey cur,
+									   int32 *set_elem_result);
+static void _bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+									  int32 set_elem_result, Datum tupdatum, bool tupnull);
+static void _bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static void _bt_array_set_low_or_high(Relation rel, ScanKey skey,
+									  BTArrayKeyInfo *array, bool low_not_high);
+static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
+static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array);
 static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir);
 static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir);
 static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
@@ -207,6 +218,7 @@ _bt_compare_array_skey(FmgrInfo *orderproc,
 	int32		result = 0;
 
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
+	Assert(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)));
 
 	if (tupnull)				/* NULL tupdatum */
 	{
@@ -283,6 +295,8 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	Datum		arrdatum;
 
 	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(!(cur->sk_flags & SK_BT_SKIP));
+	Assert(!(cur->sk_flags & SK_ISNULL));	/* SAOP arrays never have NULLs */
 	Assert(cur->sk_strategy == BTEqualStrategyNumber);
 
 	if (cur_elem_trig)
@@ -405,6 +419,186 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 	return low_elem;
 }
 
+/*
+ * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array
+ *
+ * Does not return an index into the array, since skip arrays don't really
+ * contain elements (they generate their array elements procedurally instead).
+ * Our interface matches that of _bt_binsrch_array_skey in every other way.
+ *
+ * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true
+ * array.  The value 0 indicates that tupdatum/tupnull is within the range of
+ * the skip array.  We return -1 when tupdatum/tupnull is lower that any value
+ * within the range of the array, and 1 when it is higher than every value.
+ * Caller should pass *set_elem_result to _bt_skiparray_set_element to advance
+ * the array.
+ *
+ * cur_elem_trig indicates if array advancement was triggered by this array's
+ * scan key.  We use this to optimize-away comparisons that are known by our
+ * caller to be unnecessary from context, just like _bt_binsrch_array_skey.
+ */
+static void
+_bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir,
+						   Datum tupdatum, bool tupnull,
+						   BTArrayKeyInfo *array, ScanKey cur,
+						   int32 *set_elem_result)
+{
+	Assert(cur->sk_flags & SK_BT_SKIP);
+	Assert(cur->sk_flags & SK_SEARCHARRAY);
+	Assert(cur->sk_flags & SK_BT_REQFWD);
+	Assert(array->num_elems == -1);
+	Assert(!ScanDirectionIsNoMovement(dir));
+
+	if (array->null_elem)
+	{
+		Assert(!array->low_compare && !array->high_compare);
+
+		*set_elem_result = 0;
+		return;
+	}
+
+	if (tupnull)				/* NULL tupdatum */
+	{
+		if (cur->sk_flags & SK_BT_NULLS_FIRST)
+			*set_elem_result = -1;	/* NULL "<" NOT_NULL */
+		else
+			*set_elem_result = 1;	/* NULL ">" NOT_NULL */
+		return;
+	}
+
+	/*
+	 * Array inequalities determine whether tupdatum is within the range of
+	 * caller's skip array
+	 */
+	*set_elem_result = 0;
+	if (ScanDirectionIsForward(dir))
+	{
+		/*
+		 * Evaluate low_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate high_compare
+		 */
+		if (!cur_elem_trig && array->low_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+											array->low_compare->sk_collation,
+											tupdatum,
+											array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+		else if (array->high_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												 array->high_compare->sk_collation,
+												 tupdatum,
+												 array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+	}
+	else
+	{
+		/*
+		 * Evaluate high_compare first (unless cur_elem_trig tells us that it
+		 * cannot possibly fail to be satisfied), then evaluate low_compare
+		 */
+		if (!cur_elem_trig && array->high_compare &&
+			!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+											array->high_compare->sk_collation,
+											tupdatum,
+											array->high_compare->sk_argument)))
+			*set_elem_result = 1;
+		else if (array->low_compare &&
+				 !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												 array->low_compare->sk_collation,
+												 tupdatum,
+												 array->low_compare->sk_argument)))
+			*set_elem_result = -1;
+	}
+
+	/*
+	 * Assert that any keys that were assumed to be satisfied already (due to
+	 * caller passing cur_elem_trig=true) really are satisfied as expected
+	 */
+#ifdef USE_ASSERT_CHECKING
+	if (cur_elem_trig)
+	{
+		if (ScanDirectionIsForward(dir) && array->low_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+												  array->low_compare->sk_collation,
+												  tupdatum,
+												  array->low_compare->sk_argument)));
+
+		if (ScanDirectionIsBackward(dir) && array->high_compare)
+			Assert(DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+												  array->high_compare->sk_collation,
+												  tupdatum,
+												  array->high_compare->sk_argument)));
+	}
+#endif
+}
+
+/*
+ * _bt_skiparray_set_element() -- Set skip array scan key's sk_argument
+ *
+ * Caller passes set_elem_result returned by _bt_binsrch_skiparray_skey for
+ * caller's tupdatum/tupnull.
+ *
+ * We copy tupdatum/tupnull into skey's sk_argument iff set_elem_result == 0.
+ * Otherwise, we set skey to either the lowest or highest value that's within
+ * the range of caller's skip array (whichever is the best available match to
+ * tupdatum/tupnull that is still within the range of the skip array according
+ * to _bt_binsrch_skiparray_skey/set_elem_result).
+ */
+static void
+_bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  int32 set_elem_result, Datum tupdatum, bool tupnull)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (set_elem_result)
+	{
+		/* tupdatum/tupnull is out of the range of the skip array */
+		Assert(!array->null_elem);
+
+		_bt_array_set_low_or_high(rel, skey, array, set_elem_result < 0);
+		return;
+	}
+
+	/* Advance skip array to tupdatum (or tupnull) value */
+	if (unlikely(tupnull))
+	{
+		_bt_skiparray_set_isnull(rel, skey, array);
+		return;
+	}
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* tupdatum becomes new sk_argument/new current element */
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen);
+}
+
+/*
+ * _bt_skiparray_set_isnull() -- set skip array scan key to NULL
+ */
+static void
+_bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(array->null_elem && !array->low_compare && !array->high_compare);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* NULL becomes new sk_argument/new current element */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+	skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+}
+
 /*
  * _bt_start_array_keys() -- Initialize array keys at start of a scan
  *
@@ -414,29 +608,355 @@ _bt_binsrch_array_skey(FmgrInfo *orderproc,
 void
 _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
-	int			i;
 
 	Assert(so->numArrayKeys);
 	Assert(so->qual_ok);
 
-	for (i = 0; i < so->numArrayKeys; i++)
+	for (int i = 0; i < so->numArrayKeys; i++)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		Assert(curArrayKey->num_elems > 0);
 		Assert(skey->sk_flags & SK_SEARCHARRAY);
 
-		if (ScanDirectionIsBackward(dir))
-			curArrayKey->cur_elem = curArrayKey->num_elems - 1;
-		else
-			curArrayKey->cur_elem = 0;
-		skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 	}
 	so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 }
 
+/*
+ * _bt_array_set_low_or_high() -- Set array scan key to lowest/highest element
+ *
+ * Caller also passes associated scan key, which will have its argument set to
+ * the lowest/highest array value in passing.
+ */
+static void
+_bt_array_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array,
+						  bool low_not_high)
+{
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+
+	if (array->num_elems != -1)
+	{
+		/* set low or high element for SAOP array */
+		int			set_elem = 0;
+
+		Assert(!(skey->sk_flags & SK_BT_SKIP));
+
+		if (!low_not_high)
+			set_elem = array->num_elems - 1;
+
+		/*
+		 * Just copy over array datum (only skip arrays require freeing and
+		 * allocating memory for sk_argument)
+		 */
+		array->cur_elem = set_elem;
+		skey->sk_argument = array->elem_values[set_elem];
+
+		return;
+	}
+
+	/* set low or high element for skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+	Assert(array->num_elems == -1);
+
+	/* Free memory previously allocated for sk_argument if needed */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+
+	/* Reset flags */
+	skey->sk_argument = (Datum) 0;
+	skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL |
+						SK_BT_MINVAL | SK_BT_MAXVAL |
+						SK_BT_NEXT | SK_BT_PRIOR);
+
+	if (array->null_elem &&
+		(low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0)))
+	{
+		/* Requested element (either lowest or highest) has the value NULL */
+		skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL);
+	}
+	else if (low_not_high)
+	{
+		/* Setting array to lowest element (according to low_compare) */
+		skey->sk_flags |= SK_BT_MINVAL;
+	}
+	else
+	{
+		/* Setting array to highest element (according to high_compare) */
+		skey->sk_flags |= SK_BT_MAXVAL;
+	}
+}
+
+/*
+ * _bt_array_decrement() -- decrement array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully decremented.
+ * Cannot decrement an array whose current element is already the first one.
+ */
+static bool
+_bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		uflow = false;
+	Datum		dec_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MAXVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* SAOP array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem > 0)
+		{
+			/*
+			 * Just decrement current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem--;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully decremented array */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the minimum value within the range
+	 * of a skip array (often just -inf) is never decrementable
+	 */
+	if (skey->sk_flags & SK_BT_MINVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the lowest sorting value in
+	 * the index is also NULL, we cannot decrement before first array element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "decrement" the scan key's current
+	 * element by setting the PRIOR flag.  The true prior value is determined
+	 * by repositioning to the last index tuple < existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		/* Successfully "decremented" array */
+		skey->sk_flags |= SK_BT_PRIOR;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly decrement sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST));
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Decrement" from NULL to the high_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->high_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide decremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	dec_sk_argument = array->sksup->decrement(rel, skey->sk_argument, &uflow);
+	if (unlikely(uflow))
+	{
+		/* dec_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			_bt_skiparray_set_isnull(rel, skey, array);
+
+			/* Successfully "decremented" array to NULL */
+			return true;
+		}
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/*
+	 * Successfully decremented sk_argument to a non-NULL value.  Make sure
+	 * that the decremented value is still within the range of the array.
+	 */
+	if (array->low_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func,
+										array->low_compare->sk_collation,
+										dec_sk_argument,
+										array->low_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(dec_sk_argument));
+
+		/* Cannot decrement to before first array element */
+		return false;
+	}
+
+	/* Accept value returned by opclass decrement callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = dec_sk_argument;
+
+	/* Successfully decremented array */
+	return true;
+}
+
+/*
+ * _bt_array_increment() -- increment array scan key's sk_argument
+ *
+ * Return value indicates whether caller's array was successfully incremented.
+ * Cannot increment an array whose current element is already the final one.
+ */
+static bool
+_bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array)
+{
+	bool		oflow = false;
+	Datum		inc_sk_argument;
+
+	Assert(skey->sk_flags & SK_SEARCHARRAY);
+	Assert(!(skey->sk_flags & (SK_BT_MINVAL | SK_BT_NEXT | SK_BT_PRIOR)));
+
+	/* SAOP array? */
+	if (array->num_elems != -1)
+	{
+		Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL)));
+		if (array->cur_elem < array->num_elems - 1)
+		{
+			/*
+			 * Just increment current element, and assign its datum to skey
+			 * (only skip arrays need us to free existing sk_argument memory)
+			 */
+			array->cur_elem++;
+			skey->sk_argument = array->elem_values[array->cur_elem];
+
+			/* Successfully incremented array */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Nope, this is a skip array */
+	Assert(skey->sk_flags & SK_BT_SKIP);
+
+	/*
+	 * The sentinel value that represents the maximum value within the range
+	 * of a skip array (often just +inf) is never incrementable
+	 */
+	if (skey->sk_flags & SK_BT_MAXVAL)
+		return false;
+
+	/*
+	 * When the current array element is NULL, and the highest sorting value
+	 * in the index is also NULL, we cannot increment past the final element
+	 */
+	if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		return false;
+
+	/*
+	 * Opclasses without skip support "increment" the scan key's current
+	 * element by setting the NEXT flag.  The true next value is determined by
+	 * repositioning to the first index tuple > existing sk_argument/current
+	 * array element.  Note that this works in the usual way when the scan key
+	 * is already marked ISNULL (i.e. when the current element is NULL).
+	 */
+	if (!array->sksup)
+	{
+		/* Successfully "incremented" array */
+		skey->sk_flags |= SK_BT_NEXT;
+		return true;
+	}
+
+	/*
+	 * Opclasses with skip support directly increment sk_argument
+	 */
+	if (skey->sk_flags & SK_ISNULL)
+	{
+		Assert(skey->sk_flags & SK_BT_NULLS_FIRST);
+
+		/*
+		 * Existing sk_argument/array element is NULL (for an IS NULL qual).
+		 *
+		 * "Increment" from NULL to the low_elem value provided by opclass
+		 * skip support routine.
+		 */
+		skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL);
+		skey->sk_argument = datumCopy(array->sksup->low_elem,
+									  array->attbyval, array->attlen);
+		return true;
+	}
+
+	/*
+	 * Ask opclass support routine to provide incremented copy of existing
+	 * non-NULL sk_argument
+	 */
+	inc_sk_argument = array->sksup->increment(rel, skey->sk_argument, &oflow);
+	if (unlikely(oflow))
+	{
+		/* inc_sk_argument has undefined value (so no pfree) */
+		if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST))
+		{
+			_bt_skiparray_set_isnull(rel, skey, array);
+
+			/* Successfully "incremented" array to NULL */
+			return true;
+		}
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/*
+	 * Successfully incremented sk_argument to a non-NULL value.  Make sure
+	 * that the incremented value is still within the range of the array.
+	 */
+	if (array->high_compare &&
+		!DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func,
+										array->high_compare->sk_collation,
+										inc_sk_argument,
+										array->high_compare->sk_argument)))
+	{
+		/* Keep existing sk_argument after all */
+		if (!array->attbyval)
+			pfree(DatumGetPointer(inc_sk_argument));
+
+		/* Cannot increment past final array element */
+		return false;
+	}
+
+	/* Accept value returned by opclass increment callback */
+	if (!array->attbyval && skey->sk_argument)
+		pfree(DatumGetPointer(skey->sk_argument));
+	skey->sk_argument = inc_sk_argument;
+
+	/* Successfully incremented array */
+	return true;
+}
+
 /*
  * _bt_advance_array_keys_increment() -- Advance to next set of array elements
  *
@@ -452,6 +972,7 @@ _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
 static bool
 _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
 	/*
@@ -461,29 +982,30 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 	 */
 	for (int i = so->numArrayKeys - 1; i >= 0; i--)
 	{
-		BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
-		ScanKey		skey = &so->keyData[curArrayKey->scan_key];
-		int			cur_elem = curArrayKey->cur_elem;
-		int			num_elems = curArrayKey->num_elems;
-		bool		rolled = false;
+		BTArrayKeyInfo *array = &so->arrayKeys[i];
+		ScanKey		skey = &so->keyData[array->scan_key];
 
-		if (ScanDirectionIsForward(dir) && ++cur_elem >= num_elems)
+		if (ScanDirectionIsForward(dir))
 		{
-			cur_elem = 0;
-			rolled = true;
+			if (_bt_array_increment(rel, skey, array))
+				return true;
 		}
-		else if (ScanDirectionIsBackward(dir) && --cur_elem < 0)
+		else
 		{
-			cur_elem = num_elems - 1;
-			rolled = true;
+			if (_bt_array_decrement(rel, skey, array))
+				return true;
 		}
 
-		curArrayKey->cur_elem = cur_elem;
-		skey->sk_argument = curArrayKey->elem_values[cur_elem];
-		if (!rolled)
-			return true;
+		/*
+		 * Couldn't increment (or decrement) array.  Handle array roll over.
+		 *
+		 * Start over at the array's lowest sorting value (or its highest
+		 * value, for backward scans)...
+		 */
+		_bt_array_set_low_or_high(rel, skey, array,
+								  ScanDirectionIsForward(dir));
 
-		/* Need to advance next array key, if any */
+		/* ...then increment (or decrement) next most significant array */
 	}
 
 	/*
@@ -507,7 +1029,7 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
 }
 
 /*
- * _bt_rewind_nonrequired_arrays() -- Rewind non-required arrays
+ * _bt_rewind_nonrequired_arrays() -- Rewind SAOP arrays not marked required
  *
  * Called when _bt_advance_array_keys decides to start a new primitive index
  * scan on the basis of the current scan position being before the position
@@ -539,10 +1061,15 @@ _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir)
  *
  * Note: _bt_verify_arrays_bt_first is called by an assertion to enforce that
  * everybody got this right.
+ *
+ * Note: In practice almost all SAOP arrays are marked required during
+ * preprocessing (if necessary by generating skip arrays).  It is hardly ever
+ * truly necessary to call here, but consistently doing so is simpler.
  */
 static void
 _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 {
+	Relation	rel = scan->indexRelation;
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			arrayidx = 0;
 
@@ -550,7 +1077,6 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 	{
 		ScanKey		cur = so->keyData + ikey;
 		BTArrayKeyInfo *array = NULL;
-		int			first_elem_dir;
 
 		if (!(cur->sk_flags & SK_SEARCHARRAY) ||
 			cur->sk_strategy != BTEqualStrategyNumber)
@@ -562,16 +1088,10 @@ _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir)
 		if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))
 			continue;
 
-		if (ScanDirectionIsForward(dir))
-			first_elem_dir = 0;
-		else
-			first_elem_dir = array->num_elems - 1;
+		Assert(array->num_elems != -1); /* No non-required skip arrays */
 
-		if (array->cur_elem != first_elem_dir)
-		{
-			array->cur_elem = first_elem_dir;
-			cur->sk_argument = array->elem_values[first_elem_dir];
-		}
+		_bt_array_set_low_or_high(rel, cur, array,
+								  ScanDirectionIsForward(dir));
 	}
 }
 
@@ -696,9 +1216,77 @@ _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir,
 
 		tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull);
 
-		result = _bt_compare_array_skey(&so->orderProcs[ikey],
-										tupdatum, tupnull,
-										cur->sk_argument, cur);
+		if (likely(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))))
+		{
+			/* Scankey has a valid/comparable sk_argument value */
+			result = _bt_compare_array_skey(&so->orderProcs[ikey],
+											tupdatum, tupnull,
+											cur->sk_argument, cur);
+
+			if (result == 0)
+			{
+				/*
+				 * Interpret result in a way that takes NEXT/PRIOR into
+				 * account
+				 */
+				if (cur->sk_flags & SK_BT_NEXT)
+					result = -1;
+				else if (cur->sk_flags & SK_BT_PRIOR)
+					result = 1;
+
+				Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP));
+			}
+		}
+		else
+		{
+			BTArrayKeyInfo *array = NULL;
+
+			/*
+			 * Current array element/array = scan key value is a sentinel
+			 * value that represents the lowest (or highest) possible value
+			 * that's still within the range of the array.
+			 *
+			 * Like _bt_first, we only see MINVAL keys during forwards scans
+			 * (and similarly only see MAXVAL keys during backwards scans).
+			 * Even if the scan's direction changes, we'll stop at some higher
+			 * order key before we can ever reach any MAXVAL (or MINVAL) keys.
+			 * (However, unlike _bt_first we _can_ get to keys marked either
+			 * NEXT or PRIOR, regardless of the scan's current direction.)
+			 */
+			Assert(ScanDirectionIsForward(dir) ?
+				   !(cur->sk_flags & SK_BT_MAXVAL) :
+				   !(cur->sk_flags & SK_BT_MINVAL));
+
+			/*
+			 * There are no valid sk_argument values in MINVAL/MAXVAL keys.
+			 * Check if tupdatum is within the range of skip array instead.
+			 */
+			for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++)
+			{
+				array = &so->arrayKeys[arrayidx];
+				if (array->scan_key == ikey)
+					break;
+			}
+
+			_bt_binsrch_skiparray_skey(false, dir, tupdatum, tupnull,
+									   array, cur, &result);
+
+			if (result == 0)
+			{
+				/*
+				 * tupdatum satisfies both low_compare and high_compare, so
+				 * it's time to advance the array keys.
+				 *
+				 * Note: It's possible that the skip array will "advance" from
+				 * its MINVAL (or MAXVAL) representation to an alternative,
+				 * logically equivalent representation of the same value: a
+				 * representation where the = key gets a valid datum in its
+				 * sk_argument.  This is only possible when low_compare uses
+				 * the >= strategy (or high_compare uses the <= strategy).
+				 */
+				return false;
+			}
+		}
 
 		/*
 		 * Does this comparison indicate that caller must _not_ advance the
@@ -1017,18 +1605,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (beyond_end_advance)
 		{
-			int			final_elem_dir;
-
-			if (ScanDirectionIsBackward(dir) || !array)
-				final_elem_dir = 0;
-			else
-				final_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != final_elem_dir)
-			{
-				array->cur_elem = final_elem_dir;
-				cur->sk_argument = array->elem_values[final_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsBackward(dir));
 
 			continue;
 		}
@@ -1053,18 +1632,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 */
 		if (!all_required_satisfied || cur->sk_attno > tupnatts)
 		{
-			int			first_elem_dir;
-
-			if (ScanDirectionIsForward(dir) || !array)
-				first_elem_dir = 0;
-			else
-				first_elem_dir = array->num_elems - 1;
-
-			if (array && array->cur_elem != first_elem_dir)
-			{
-				array->cur_elem = first_elem_dir;
-				cur->sk_argument = array->elem_values[first_elem_dir];
-			}
+			if (array)
+				_bt_array_set_low_or_high(rel, cur, array,
+										  ScanDirectionIsForward(dir));
 
 			continue;
 		}
@@ -1080,14 +1650,22 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			bool		cur_elem_trig = (sktrig_required && ikey == sktrig);
 
 			/*
-			 * Binary search for closest match that's available from the array
+			 * "Binary search" by checking if tupdatum/tupnull are within the
+			 * range of the skip array
 			 */
-			set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
-											  cur_elem_trig, dir,
-											  tupdatum, tupnull, array, cur,
-											  &result);
+			if (array->num_elems == -1)
+				_bt_binsrch_skiparray_skey(cur_elem_trig, dir,
+										   tupdatum, tupnull, array, cur,
+										   &result);
 
-			Assert(set_elem >= 0 && set_elem < array->num_elems);
+			/*
+			 * Binary search for the closest match from the SAOP array
+			 */
+			else
+				set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey],
+												  cur_elem_trig, dir,
+												  tupdatum, tupnull, array, cur,
+												  &result);
 		}
 		else
 		{
@@ -1163,11 +1741,21 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			}
 		}
 
-		/* Advance array keys, even when set_elem isn't an exact match */
-		if (array && array->cur_elem != set_elem)
+		/* Advance array keys, even when we don't have an exact match */
+		if (array)
 		{
-			array->cur_elem = set_elem;
-			cur->sk_argument = array->elem_values[set_elem];
+			if (array->num_elems == -1)
+			{
+				/* Skip array's new element is tupdatum (or MINVAL/MAXVAL) */
+				_bt_skiparray_set_element(rel, cur, array, result,
+										  tupdatum, tupnull);
+			}
+			else if (array->cur_elem != set_elem)
+			{
+				/* SAOP array's new element is set_elem datum */
+				array->cur_elem = set_elem;
+				cur->sk_argument = array->elem_values[set_elem];
+			}
 		}
 	}
 
@@ -1581,10 +2169,11 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
 		if (array->scan_key != ikey)
 			return false;
 
-		if (array->num_elems <= 0)
+		if (array->num_elems == 0 || array->num_elems < -1)
 			return false;
 
-		if (cur->sk_argument != array->elem_values[array->cur_elem])
+		if (array->num_elems != -1 &&
+			cur->sk_argument != array->elem_values[array->cur_elem])
 			return false;
 		if (last_sk_attno > cur->sk_attno)
 			return false;
@@ -1914,6 +2503,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			continue;
 		}
 
+		/*
+		 * A skip array scan key uses one of several sentinel values.  We just
+		 * fall back on _bt_tuple_before_array_skeys when we see such a value.
+		 */
+		if (key->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL |
+							 SK_BT_NEXT | SK_BT_PRIOR))
+		{
+			Assert(key->sk_flags & SK_SEARCHARRAY);
+			Assert(key->sk_flags & SK_BT_SKIP);
+
+			*continuescan = false;
+			return false;
+		}
+
 		/* row-comparison keys need special processing */
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
@@ -1939,6 +2542,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			else
 			{
 				Assert(key->sk_flags & SK_SEARCHNOTNULL);
+				Assert(!(key->sk_flags & SK_BT_SKIP));
 				if (!isNull)
 					continue;	/* tuple satisfies this qual */
 			}
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index dd6f5a15c..817ad358f 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -106,6 +106,10 @@ btvalidate(Oid opclassoid)
 			case BTOPTIONS_PROC:
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
+			case BTSKIPSUPPORT_PROC:
+				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+											1, 1, INTERNALOID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 8546366ee..a6dd8eab5 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -1331,6 +1331,31 @@ assignProcTypes(OpFamilyMember *member, Oid amoid, Oid typeoid,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 						 errmsg("ordering equal image functions must not be cross-type")));
 		}
+		else if (member->number == BTSKIPSUPPORT_PROC)
+		{
+			if (procform->pronargs != 1 ||
+				procform->proargtypes.values[0] != INTERNALOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must accept type \"internal\"")));
+			if (procform->prorettype != VOIDOID)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must return void")));
+
+			/*
+			 * pg_amproc functions are indexed by (lefttype, righttype), but a
+			 * skip support function doesn't make sense in cross-type
+			 * scenarios.  The same opclass opcintype OID is always used for
+			 * lefttype and righttype.  Providing a cross-type routine isn't
+			 * sensible.  Reject cross-type ALTER OPERATOR FAMILY ...  ADD
+			 * FUNCTION 6 statements here.
+			 */
+			if (member->lefttype != member->righttype)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("btree skip support functions must not be cross-type")));
+		}
 	}
 	else if (GetIndexAmRoutineByAmId(amoid, false)->amcanhash)
 	{
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 35e8c01aa..4a233b63c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -99,6 +99,7 @@ OBJS = \
 	rowtypes.o \
 	ruleutils.o \
 	selfuncs.o \
+	skipsupport.o \
 	tid.o \
 	timestamp.o \
 	trigfuncs.o \
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index f279853de..4227ab1a7 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -34,6 +34,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -462,6 +463,51 @@ date_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+static Datum
+date_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOBEGIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return DateADTGetDatum(dexisting - 1);
+}
+
+static Datum
+date_increment(Relation rel, Datum existing, bool *overflow)
+{
+	DateADT		dexisting = DatumGetDateADT(existing);
+
+	if (dexisting == DATEVAL_NOEND)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return DateADTGetDatum(dexisting + 1);
+}
+
+Datum
+date_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = date_decrement;
+	sksup->increment = date_increment;
+	sksup->low_elem = DateADTGetDatum(DATEVAL_NOBEGIN);
+	sksup->high_elem = DateADTGetDatum(DATEVAL_NOEND);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 hashdate(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index f23cfad71..244f48f4f 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -86,6 +86,7 @@ backend_sources += files(
   'rowtypes.c',
   'ruleutils.c',
   'selfuncs.c',
+  'skipsupport.c',
   'tid.c',
   'timestamp.c',
   'trigfuncs.c',
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 5b35debc8..385c20c62 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -193,6 +193,8 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid,
 										  bool *failure);
 static void examine_simple_variable(PlannerInfo *root, Var *var,
 									VariableStatData *vardata);
+static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+									  int indexcol, VariableStatData *vardata);
 static bool get_variable_range(PlannerInfo *root, VariableStatData *vardata,
 							   Oid sortop, Oid collation,
 							   Datum *min, Datum *max);
@@ -214,6 +216,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
+static double btcost_correlation(IndexOptInfo *index,
+								 VariableStatData *vardata);
 
 
 /*
@@ -5943,6 +5947,92 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 }
 
+/*
+ * examine_indexcol_variable
+ *		Try to look up statistical data about an index column/expression.
+ *		Fill in a VariableStatData struct to describe the column.
+ *
+ * Inputs:
+ *	root: the planner info
+ *	index: the index whose column we're interested in
+ *	indexcol: 0-based index column number (subscripts index->indexkeys[])
+ *
+ * Outputs: *vardata is filled as follows:
+ *	var: the input expression (with any binary relabeling stripped, if
+ *		it is or contains a variable; but otherwise the type is preserved)
+ *	rel: RelOptInfo for table relation containing variable.
+ *	statsTuple: the pg_statistic entry for the variable, if one exists;
+ *		otherwise NULL.
+ *	freefunc: pointer to a function to release statsTuple with.
+ *
+ * Caller is responsible for doing ReleaseVariableStats() before exiting.
+ */
+static void
+examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
+						  int indexcol, VariableStatData *vardata)
+{
+	AttrNumber	colnum;
+	Oid			relid;
+
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* Simple variable --- look to stats for the underlying table */
+		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+
+		Assert(rte->rtekind == RTE_RELATION);
+		relid = rte->relid;
+		Assert(relid != InvalidOid);
+		colnum = index->indexkeys[indexcol];
+		vardata->rel = index->rel;
+
+		if (get_relation_stats_hook &&
+			(*get_relation_stats_hook) (root, rte, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+	else
+	{
+		/* Expression --- maybe there are stats for the index itself */
+		relid = index->indexoid;
+		colnum = indexcol + 1;
+
+		if (get_index_stats_hook &&
+			(*get_index_stats_hook) (root, relid, colnum, vardata))
+		{
+			/*
+			 * The hook took control of acquiring a stats tuple.  If it did
+			 * supply a tuple, it'd better have supplied a freefunc.
+			 */
+			if (HeapTupleIsValid(vardata->statsTuple) &&
+				!vardata->freefunc)
+				elog(ERROR, "no function provided to release variable stats with");
+		}
+		else
+		{
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(relid),
+												  Int16GetDatum(colnum),
+												  BoolGetDatum(false));
+			vardata->freefunc = ReleaseSysCache;
+		}
+	}
+}
+
 /*
  * Check whether it is permitted to call func_oid passing some of the
  * pg_statistic data in vardata.  We allow this either if the user has SELECT
@@ -7001,6 +7091,53 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	return list_concat(predExtraQuals, indexQuals);
 }
 
+/*
+ * Estimate correlation of btree index's first column.
+ *
+ * If we can get an estimate of the first column's ordering correlation C
+ * from pg_statistic, estimate the index correlation as C for a single-column
+ * index, or C * 0.75 for multiple columns.  The idea here is that multiple
+ * columns dilute the importance of the first column's ordering, but don't
+ * negate it entirely.
+ *
+ * We already filled in the stats tuple for *vardata when called.
+ */
+static double
+btcost_correlation(IndexOptInfo *index, VariableStatData *vardata)
+{
+	Oid			sortop;
+	AttStatsSlot sslot;
+	double		indexCorrelation = 0;
+
+	Assert(HeapTupleIsValid(vardata->statsTuple));
+
+	sortop = get_opfamily_member(index->opfamily[0],
+								 index->opcintype[0],
+								 index->opcintype[0],
+								 BTLessStrategyNumber);
+	if (OidIsValid(sortop) &&
+		get_attstatsslot(&sslot, vardata->statsTuple,
+						 STATISTIC_KIND_CORRELATION, sortop,
+						 ATTSTATSSLOT_NUMBERS))
+	{
+		double		varCorrelation;
+
+		Assert(sslot.nnumbers == 1);
+		varCorrelation = sslot.numbers[0];
+
+		if (index->reverse_sort[0])
+			varCorrelation = -varCorrelation;
+
+		if (index->nkeycolumns > 1)
+			indexCorrelation = varCorrelation * 0.75;
+		else
+			indexCorrelation = varCorrelation;
+
+		free_attstatsslot(&sslot);
+	}
+
+	return indexCorrelation;
+}
 
 void
 btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
@@ -7010,17 +7147,19 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 {
 	IndexOptInfo *index = path->indexinfo;
 	GenericCosts costs = {0};
-	Oid			relid;
-	AttrNumber	colnum;
 	VariableStatData vardata = {0};
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
+	List	   *indexSkipQuals;
 	int			indexcol;
 	bool		eqQualHere;
-	bool		found_saop;
+	bool		found_row_compare;
+	bool		found_array;
 	bool		found_is_null_op;
+	bool		have_correlation = false;
 	double		num_sa_scans;
+	double		correlation = 0.0;
 	ListCell   *lc;
 
 	/*
@@ -7031,19 +7170,24 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * it's OK to count them in indexSelectivity, but they should not count
 	 * for estimating numIndexTuples.  So we must examine the given indexquals
 	 * to find out which ones count as boundary quals.  We rely on the
-	 * knowledge that they are given in index column order.
+	 * knowledge that they are given in index column order.  Note that nbtree
+	 * preprocessing can add skip arrays that act as leading '=' quals in the
+	 * absence of ordinary input '=' quals, so in practice _most_ input quals
+	 * are able to act as index bound quals (which we take into account here).
 	 *
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 *
-	 * If there's a ScalarArrayOpExpr in the quals, we'll actually perform up
-	 * to N index descents (not just one), but the ScalarArrayOpExpr's
+	 * If there's a SAOP or skip array in the quals, we'll actually perform up
+	 * to N index descents (not just one), but the underlying array key's
 	 * operator can be considered to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
+	indexSkipQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
-	found_saop = false;
+	found_row_compare = false;
+	found_array = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -7051,17 +7195,203 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		IndexClause *iclause = lfirst_node(IndexClause, lc);
 		ListCell   *lc2;
 
-		if (indexcol != iclause->indexcol)
+		if (indexcol < iclause->indexcol)
 		{
-			/* Beginning of a new column's quals */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			double		num_sa_scans_prev_cols = num_sa_scans;
+
+			/*
+			 * Beginning of a new column's quals.
+			 *
+			 * Skip scans use skip arrays, which are ScalarArrayOp style
+			 * arrays that generate their elements procedurally and on demand.
+			 * Given a composite index on "(a, b)", and an SQL WHERE clause
+			 * "WHERE b = 42", a skip scan will effectively use an indexqual
+			 * "WHERE a = ANY('{every col a value}') AND b = 42".  (Obviously,
+			 * the array on "a" must also return "IS NULL" matches, since our
+			 * WHERE clause used no strict operator on "a").
+			 *
+			 * Here we consider how nbtree will backfill skip arrays for any
+			 * index columns that lacked an '=' qual.  This maintains our
+			 * num_sa_scans estimate, and determines if this new column (the
+			 * "iclause->indexcol" column, not the prior "indexcol" column)
+			 * can have its RestrictInfos/quals added to indexBoundQuals.
+			 *
+			 * We'll need to handle columns that have inequality quals, where
+			 * the skip array generates values from a range constrained by the
+			 * quals (not every possible value).  We've been maintaining
+			 * indexSkipQuals to help with this; it will now contain all of
+			 * the prior column's quals (that is, indexcol's quals) when they
+			 * might be used for this.
+			 */
+			if (found_row_compare)
+			{
+				/*
+				 * Skip arrays can't be added after a RowCompare input qual
+				 * due to limitations in nbtree
+				 */
+				break;
+			}
+			if (eqQualHere)
+			{
+				/*
+				 * Don't need to add a skip array for an indexcol that already
+				 * has an '=' qual/equality constraint
+				 */
+				indexcol++;
+				indexSkipQuals = NIL;
+			}
 			eqQualHere = false;
-			indexcol++;
+
+			while (indexcol < iclause->indexcol)
+			{
+				double		ndistinct;
+				bool		isdefault = true;
+
+				found_array = true;
+
+				/*
+				 * A skipped attribute's ndistinct forms the basis of our
+				 * estimate of the total number of "array elements" used by
+				 * its skip array at runtime.  Look that up first.
+				 */
+				examine_indexcol_variable(root, index, indexcol, &vardata);
+				ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+
+				if (indexcol == 0)
+				{
+					/*
+					 * Get an estimate of the leading column's correlation in
+					 * passing (avoids rereading variable stats below)
+					 */
+					if (HeapTupleIsValid(vardata.statsTuple))
+						correlation = btcost_correlation(index, &vardata);
+					have_correlation = true;
+				}
+
+				ReleaseVariableStats(vardata);
+
+				/*
+				 * If ndistinct is a default estimate, conservatively assume
+				 * that no skipping will happen at runtime
+				 */
+				if (isdefault)
+				{
+					num_sa_scans = num_sa_scans_prev_cols;
+					break;		/* done building indexBoundQuals */
+				}
+
+				/*
+				 * Apply indexcol's indexSkipQuals selectivity to ndistinct
+				 */
+				if (indexSkipQuals != NIL)
+				{
+					List	   *partialSkipQuals;
+					Selectivity ndistinctfrac;
+
+					/*
+					 * If the index is partial, AND the index predicate with
+					 * the index-bound quals to produce a more accurate idea
+					 * of the number of distinct values for prior indexcol
+					 */
+					partialSkipQuals = add_predicate_to_index_quals(index,
+																	indexSkipQuals);
+
+					ndistinctfrac = clauselist_selectivity(root, partialSkipQuals,
+														   index->rel->relid,
+														   JOIN_INNER,
+														   NULL);
+
+					/*
+					 * If ndistinctfrac is selective (on its own), the scan is
+					 * unlikely to benefit from repositioning itself using
+					 * later quals.  Do not allow iclause->indexcol's quals to
+					 * be added to indexBoundQuals (it would increase descent
+					 * costs, without lowering numIndexTuples costs by much).
+					 */
+					if (ndistinctfrac < DEFAULT_RANGE_INEQ_SEL)
+					{
+						num_sa_scans = num_sa_scans_prev_cols;
+						break;	/* done building indexBoundQuals */
+					}
+
+					/* Adjust ndistinct downward */
+					ndistinct = rint(ndistinct * ndistinctfrac);
+					ndistinct = Max(ndistinct, 1);
+				}
+
+				/*
+				 * When there's no inequality quals, account for the need to
+				 * find an initial value by counting -inf/+inf as a value.
+				 *
+				 * We don't charge anything extra for possible next/prior key
+				 * index probes, which are sometimes used to find the next
+				 * valid skip array element (ahead of using the located
+				 * element value to relocate the scan to the next position
+				 * that might contain matching tuples).  It seems hard to do
+				 * better here.  Use of the skip support infrastructure often
+				 * avoids most next/prior key probes.  But even when it can't,
+				 * there's a decent chance that most individual next/prior key
+				 * probes will locate a leaf page whose key space overlaps all
+				 * of the scan's keys (even the lower-order keys) -- which
+				 * also avoids the need for a separate, extra index descent.
+				 * Note also that these probes are much cheaper than non-probe
+				 * primitive index scans: they're reliably very selective.
+				 */
+				if (indexSkipQuals == NIL)
+					ndistinct += 1;
+
+				/*
+				 * Update num_sa_scans estimate by multiplying by ndistinct.
+				 *
+				 * We make the pessimistic assumption that there is no
+				 * naturally occurring cross-column correlation.  This is
+				 * often wrong, but it seems best to err on the side of not
+				 * expecting skipping to be helpful...
+				 */
+				num_sa_scans *= ndistinct;
+
+				/*
+				 * ...but back out of adding this latest group of 1 or more
+				 * skip arrays when num_sa_scans exceeds the total number of
+				 * index pages (revert to num_sa_scans from before indexcol).
+				 * This causes a sharp discontinuity in cost (as a function of
+				 * the indexcol's ndistinct), but that is representative of
+				 * actual runtime costs.
+				 *
+				 * Note that skipping is helpful when each primitive index
+				 * scan only manages to skip over 1 or 2 irrelevant leaf pages
+				 * on average.  Skip arrays bring savings in CPU costs due to
+				 * the scan not needing to evaluate indexquals against every
+				 * tuple, which can greatly exceed any savings in I/O costs.
+				 * This test is a test of whether num_sa_scans implies that
+				 * we're past the point where the ability to skip ceases to
+				 * lower the scan's costs (even qual evaluation CPU costs).
+				 */
+				if (index->pages < num_sa_scans)
+				{
+					num_sa_scans = num_sa_scans_prev_cols;
+					break;		/* done building indexBoundQuals */
+				}
+
+				indexcol++;
+				indexSkipQuals = NIL;
+			}
+
+			/*
+			 * Finished considering the need to add skip arrays to bridge an
+			 * initial eqQualHere gap between the old and new index columns
+			 * (or there was no initial eqQualHere gap in the first place).
+			 *
+			 * If an initial gap could not be bridged, then new column's quals
+			 * (i.e. iclause->indexcol's quals) won't go into indexBoundQuals,
+			 * and so won't affect our final numIndexTuples estimate.
+			 */
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+				break;			/* done building indexBoundQuals */
 		}
 
+		Assert(indexcol == iclause->indexcol);
+
 		/* Examine each indexqual associated with this index clause */
 		foreach(lc2, iclause->indexquals)
 		{
@@ -7081,6 +7411,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
 				clause_op = linitial_oid(rc->opnos);
+				found_row_compare = true;
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
@@ -7089,7 +7420,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				double		alength = estimate_array_length(root, other_operand);
 
 				clause_op = saop->opno;
-				found_saop = true;
+				found_array = true;
 				/* estimate SA descents by indexBoundQuals only */
 				if (alength > 1)
 					num_sa_scans *= alength;
@@ -7101,7 +7432,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				if (nt->nulltesttype == IS_NULL)
 				{
 					found_is_null_op = true;
-					/* IS NULL is like = for selectivity purposes */
+					/* IS NULL is like = for selectivity/skip scan purposes */
 					eqQualHere = true;
 				}
 			}
@@ -7120,19 +7451,28 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 			}
 
 			indexBoundQuals = lappend(indexBoundQuals, rinfo);
+
+			/*
+			 * We apply inequality selectivities to estimate index descent
+			 * costs with scans that use skip arrays.  Save this indexcol's
+			 * RestrictInfos if it looks like they'll be needed for that.
+			 */
+			if (!eqQualHere && !found_row_compare &&
+				indexcol < index->nkeycolumns - 1)
+				indexSkipQuals = lappend(indexSkipQuals, rinfo);
 		}
 	}
 
 	/*
 	 * If index is unique and we found an '=' clause for each column, we can
 	 * just assume numIndexTuples = 1 and skip the expensive
-	 * clauselist_selectivity calculations.  However, a ScalarArrayOp or
-	 * NullTest invalidates that theory, even though it sets eqQualHere.
+	 * clauselist_selectivity calculations.  However, an array or NullTest
+	 * always invalidates that theory (even when eqQualHere has been set).
 	 */
 	if (index->unique &&
 		indexcol == index->nkeycolumns - 1 &&
 		eqQualHere &&
-		!found_saop &&
+		!found_array &&
 		!found_is_null_op)
 		numIndexTuples = 1.0;
 	else
@@ -7154,7 +7494,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		numIndexTuples = btreeSelectivity * index->rel->tuples;
 
 		/*
-		 * btree automatically combines individual ScalarArrayOpExpr primitive
+		 * btree automatically combines individual array element primitive
 		 * index scans whenever the tuples covered by the next set of array
 		 * keys are close to tuples covered by the current set.  That puts a
 		 * natural ceiling on the worst case number of descents -- there
@@ -7172,16 +7512,18 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * of leaf pages (we make it 1/3 the total number of pages instead) to
 		 * give the btree code credit for its ability to continue on the leaf
 		 * level with low selectivity scans.
+		 *
+		 * Note: num_sa_scans includes both ScalarArrayOp array elements and
+		 * skip array elements whose qual affects our numIndexTuples estimate.
 		 */
 		num_sa_scans = Min(num_sa_scans, ceil(index->pages * 0.3333333));
 		num_sa_scans = Max(num_sa_scans, 1);
 
 		/*
-		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
-		 * to integer.
+		 * As in genericcostestimate(), we have to adjust for any array quals
+		 * included in indexBoundQuals, and then round to integer.
 		 *
-		 * It is tempting to make genericcostestimate behave as if SAOP
+		 * It is tempting to make genericcostestimate behave as if array
 		 * clauses work in almost the same way as scalar operators during
 		 * btree scans, making the top-level scan look like a continuous scan
 		 * (as opposed to num_sa_scans-many primitive index scans).  After
@@ -7214,7 +7556,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * comparisons to descend a btree of N leaf tuples.  We charge one
 	 * cpu_operator_cost per comparison.
 	 *
-	 * If there are ScalarArrayOpExprs, charge this once per estimated SA
+	 * If there are SAOP/skip array keys, charge this once per estimated SA
 	 * index descent.  The ones after the first one are not startup cost so
 	 * far as the overall plan goes, so just add them to "total" cost.
 	 */
@@ -7234,110 +7576,25 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * cost is somewhat arbitrarily set at 50x cpu_operator_cost per page
 	 * touched.  The number of such pages is btree tree height plus one (ie,
 	 * we charge for the leaf page too).  As above, charge once per estimated
-	 * SA index descent.
+	 * SAOP/skip array descent.
 	 */
 	descentCost = (index->tree_height + 1) * DEFAULT_PAGE_CPU_MULTIPLIER * cpu_operator_cost;
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
-	/*
-	 * If we can get an estimate of the first column's ordering correlation C
-	 * from pg_statistic, estimate the index correlation as C for a
-	 * single-column index, or C * 0.75 for multiple columns. (The idea here
-	 * is that multiple columns dilute the importance of the first column's
-	 * ordering, but don't negate it entirely.  Before 8.0 we divided the
-	 * correlation by the number of columns, but that seems too strong.)
-	 */
-	if (index->indexkeys[0] != 0)
+	if (!have_correlation)
 	{
-		/* Simple variable --- look to stats for the underlying table */
-		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
-
-		Assert(rte->rtekind == RTE_RELATION);
-		relid = rte->relid;
-		Assert(relid != InvalidOid);
-		colnum = index->indexkeys[0];
-
-		if (get_relation_stats_hook &&
-			(*get_relation_stats_hook) (root, rte, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(rte->inh));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		examine_indexcol_variable(root, index, 0, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+			costs.indexCorrelation = btcost_correlation(index, &vardata);
+		ReleaseVariableStats(vardata);
 	}
 	else
 	{
-		/* Expression --- maybe there are stats for the index itself */
-		relid = index->indexoid;
-		colnum = 1;
-
-		if (get_index_stats_hook &&
-			(*get_index_stats_hook) (root, relid, colnum, &vardata))
-		{
-			/*
-			 * The hook took control of acquiring a stats tuple.  If it did
-			 * supply a tuple, it'd better have supplied a freefunc.
-			 */
-			if (HeapTupleIsValid(vardata.statsTuple) &&
-				!vardata.freefunc)
-				elog(ERROR, "no function provided to release variable stats with");
-		}
-		else
-		{
-			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-												 ObjectIdGetDatum(relid),
-												 Int16GetDatum(colnum),
-												 BoolGetDatum(false));
-			vardata.freefunc = ReleaseSysCache;
-		}
+		/* btcost_correlation already called earlier on */
+		costs.indexCorrelation = correlation;
 	}
 
-	if (HeapTupleIsValid(vardata.statsTuple))
-	{
-		Oid			sortop;
-		AttStatsSlot sslot;
-
-		sortop = get_opfamily_member(index->opfamily[0],
-									 index->opcintype[0],
-									 index->opcintype[0],
-									 BTLessStrategyNumber);
-		if (OidIsValid(sortop) &&
-			get_attstatsslot(&sslot, vardata.statsTuple,
-							 STATISTIC_KIND_CORRELATION, sortop,
-							 ATTSTATSSLOT_NUMBERS))
-		{
-			double		varCorrelation;
-
-			Assert(sslot.nnumbers == 1);
-			varCorrelation = sslot.numbers[0];
-
-			if (index->reverse_sort[0])
-				varCorrelation = -varCorrelation;
-
-			if (index->nkeycolumns > 1)
-				costs.indexCorrelation = varCorrelation * 0.75;
-			else
-				costs.indexCorrelation = varCorrelation;
-
-			free_attstatsslot(&sslot);
-		}
-	}
-
-	ReleaseVariableStats(vardata);
-
 	*indexStartupCost = costs.indexStartupCost;
 	*indexTotalCost = costs.indexTotalCost;
 	*indexSelectivity = costs.indexSelectivity;
diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c
new file mode 100644
index 000000000..2bd35d2d2
--- /dev/null
+++ b/src/backend/utils/adt/skipsupport.c
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * skipsupport.c
+ *	  Support routines for B-Tree skip scan.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/skipsupport.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "utils/lsyscache.h"
+#include "utils/skipsupport.h"
+
+/*
+ * Fill in SkipSupport given an operator class (opfamily + opcintype).
+ *
+ * On success, returns skip support struct, allocating in caller's memory
+ * context.  Otherwise returns NULL, indicating that operator class has no
+ * skip support function.
+ */
+SkipSupport
+PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse)
+{
+	Oid			skipSupportFunction;
+	SkipSupport sksup;
+
+	/* Look for a skip support function */
+	skipSupportFunction = get_opfamily_proc(opfamily, opcintype, opcintype,
+											BTSKIPSUPPORT_PROC);
+	if (!OidIsValid(skipSupportFunction))
+		return NULL;
+
+	sksup = palloc(sizeof(SkipSupportData));
+	OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup));
+
+	if (reverse)
+	{
+		/*
+		 * DESC/reverse case: swap low_elem with high_elem, and swap decrement
+		 * with increment
+		 */
+		Datum		low_elem = sksup->low_elem;
+		SkipSupportIncDec decrement = sksup->decrement;
+
+		sksup->low_elem = sksup->high_elem;
+		sksup->decrement = sksup->increment;
+
+		sksup->high_elem = low_elem;
+		sksup->increment = decrement;
+	}
+
+	return sksup;
+}
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 9682f9dbd..347089b76 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -37,6 +37,7 @@
 #include "utils/datetime.h"
 #include "utils/float.h"
 #include "utils/numeric.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 
 /*
@@ -2304,6 +2305,53 @@ timestamp_sortsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MIN)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return TimestampGetDatum(texisting - 1);
+}
+
+/* note: this is used for timestamptz also */
+static Datum
+timestamp_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Timestamp	texisting = DatumGetTimestamp(existing);
+
+	if (texisting == PG_INT64_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return TimestampGetDatum(texisting + 1);
+}
+
+Datum
+timestamp_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = timestamp_decrement;
+	sksup->increment = timestamp_increment;
+	sksup->low_elem = TimestampGetDatum(PG_INT64_MIN);
+	sksup->high_elem = TimestampGetDatum(PG_INT64_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 timestamp_hash(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index be0f0f9f1..bce7309c1 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -13,6 +13,7 @@
 
 #include "postgres.h"
 
+#include <limits.h>
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
@@ -21,6 +22,7 @@
 #include "port/pg_bswap.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc.h"
+#include "utils/skipsupport.h"
 #include "utils/sortsupport.h"
 #include "utils/timestamp.h"
 #include "utils/uuid.h"
@@ -418,6 +420,74 @@ uuid_abbrev_convert(Datum original, SortSupport ssup)
 	return res;
 }
 
+static Datum
+uuid_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] > 0)
+		{
+			uuid->data[i]--;
+			*underflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = UCHAR_MAX;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*underflow = true;
+	return (Datum) 0;
+}
+
+static Datum
+uuid_increment(Relation rel, Datum existing, bool *overflow)
+{
+	pg_uuid_t  *uuid;
+
+	uuid = (pg_uuid_t *) palloc(UUID_LEN);
+	memcpy(uuid, DatumGetUUIDP(existing), UUID_LEN);
+	for (int i = UUID_LEN - 1; i >= 0; i--)
+	{
+		if (uuid->data[i] < UCHAR_MAX)
+		{
+			uuid->data[i]++;
+			*overflow = false;
+			return UUIDPGetDatum(uuid);
+		}
+		uuid->data[i] = 0;
+	}
+
+	pfree(uuid);				/* cannot leak memory */
+
+	/* return value is undefined */
+	*overflow = true;
+	return (Datum) 0;
+}
+
+Datum
+uuid_skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+	pg_uuid_t  *uuid_min = palloc(UUID_LEN);
+	pg_uuid_t  *uuid_max = palloc(UUID_LEN);
+
+	memset(uuid_min->data, 0x00, UUID_LEN);
+	memset(uuid_max->data, 0xFF, UUID_LEN);
+
+	sksup->decrement = uuid_decrement;
+	sksup->increment = uuid_increment;
+	sksup->low_elem = UUIDPGetDatum(uuid_min);
+	sksup->high_elem = UUIDPGetDatum(uuid_max);
+
+	PG_RETURN_VOID();
+}
+
 /* hash index support */
 Datum
 uuid_hash(PG_FUNCTION_ARGS)
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 2b3997988..3e6f30d74 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and four optional support functions.  The five
+  one required and five optional support functions.  The six
   user-defined methods are:
  </para>
  <variablelist>
@@ -583,6 +583,38 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
     </para>
    </listitem>
   </varlistentry>
+  <varlistentry>
+   <term><function>skipsupport</function></term>
+   <listitem>
+    <para>
+     Optionally, a btree operator family may provide a <firstterm>skip
+      support</firstterm> function, registered under support function
+     number 6.  These functions allow the B-tree code to more efficiently
+     navigate the index structure during an index skip scan.  Operator classes
+     that implement skip support provide the core B-Tree code with a way of
+     enumerating and iterating through every possible value from the domain of
+     indexable values.   The APIs involved in this are defined in
+     <filename>src/include/utils/skipsupport.h</filename>.
+    </para>
+    <para>
+     Operator classes that do not provide a skip support function are still
+     eligible to use skip scan.  The core code can still use a fallback
+     strategy, though it might be somewhat less efficient with discrete types.
+     It usually doesn't make sense (and may not even be feasible) for operator
+     classes on continuous types to provide a skip support function.
+    </para>
+    <para>
+     It is not sensible for an operator family to register a cross-type
+     <function>skipsupport</function> function, and attempting to do so will
+     result in an error.  This is because determining the next indexable value
+     from some earlier value does not just depend on sorting/equality
+     semantics, which are more or less defined at the operator family level.
+     Skip scan works by exhaustively considering every possible value that
+     might be stored in an index, so the domain of the particular data type
+     stored within the index (the input opclass type) must also be considered.
+    </para>
+   </listitem>
+  </varlistentry>
  </variablelist>
 
 </sect2>
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index 768b77aa0..d5adb58c1 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -835,7 +835,8 @@ amrestrpos (IndexScanDesc scan);
   <para>
 <programlisting>
 Size
-amestimateparallelscan (int nkeys,
+amestimateparallelscan (Relation indexRelation,
+                        int nkeys,
                         int norderbys);
 </programlisting>
    Estimate and return the number of bytes of dynamic shared memory which
diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 6d731e070..b0cb09eb7 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -457,23 +457,26 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   <para>
    A multicolumn B-tree index can be used with query conditions that
    involve any subset of the index's columns, but the index is most
-   efficient when there are constraints on the leading (leftmost) columns.
-   The exact rule is that equality constraints on leading columns, plus
-   any inequality constraints on the first column that does not have an
-   equality constraint, will be used to limit the portion of the index
-   that is scanned.  Constraints on columns to the right of these columns
-   are checked in the index, so they save visits to the table proper, but
-   they do not reduce the portion of the index that has to be scanned.
+   efficient when there are equality constraints on the leading (leftmost) columns.
+   B-Tree index scans can use the index skip scan strategy to generate
+   equality constraints on prefix columns that were wholly omitted from the
+   query predicate, as well as prefix columns whose values were constrained by
+   inequality conditions.
    For example, given an index on <literal>(a, b, c)</literal> and a
    query condition <literal>WHERE a = 5 AND b &gt;= 42 AND c &lt; 77</literal>,
    the index would have to be scanned from the first entry with
    <literal>a</literal> = 5 and <literal>b</literal> = 42 up through the last entry with
-   <literal>a</literal> = 5.  Index entries with <literal>c</literal> &gt;= 77 would be
-   skipped, but they'd still have to be scanned through.
+   <literal>a</literal> = 5.  Intervening groups of index entries with
+   <literal>c</literal> &gt;= 77 would not need to be returned by the scan,
+   and can be skipped over entirely by applying the skip scan strategy.
    This index could in principle be used for queries that have constraints
    on <literal>b</literal> and/or <literal>c</literal> with no constraint on <literal>a</literal>
-   &mdash; but the entire index would have to be scanned, so in most cases
-   the planner would prefer a sequential table scan over using the index.
+   &mdash; but that approach is generally only taken when there are so few
+   distinct <literal>a</literal> values that the planner expects the skip scan
+   strategy to allow the scan to skip over most individual index leaf pages.
+   If there are many distinct <literal>a</literal> values, then the entire
+   index will have to be scanned, so in most cases the planner will prefer a
+   sequential table scan over using the index.
   </para>
 
   <para>
@@ -508,11 +511,15 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor);
   </para>
 
   <para>
-   Multicolumn indexes should be used sparingly.  In most situations,
-   an index on a single column is sufficient and saves space and time.
-   Indexes with more than three columns are unlikely to be helpful
-   unless the usage of the table is extremely stylized.  See also
-   <xref linkend="indexes-bitmap-scans"/> and
+   Multicolumn indexes should only be used when testing shows that they'll
+   offer a clear advantage over simply using multiple single column indexes.
+   Indexes with more than three columns can make sense, but only when most
+   queries that make use of later columns also make use of earlier prefix
+   columns.  It's possible for B-Tree index scans to make use of <quote>skip
+    scan</quote> optimizations with queries that omit a low cardinality
+   leading prefix column, but this is usually much less efficient than a scan
+   of an index without the extra prefix column.  See <xref
+    linkend="indexes-bitmap-scans"/> and
    <xref linkend="indexes-index-only-scans"/> for some discussion of the
    merits of different index configurations.
   </para>
@@ -669,9 +676,13 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);
    multicolumn index on <literal>(x, y)</literal>.  This index would typically be
    more efficient than index combination for queries involving both
    columns, but as discussed in <xref linkend="indexes-multicolumn"/>, it
-   would be almost useless for queries involving only <literal>y</literal>, so it
-   should not be the only index.  A combination of the multicolumn index
-   and a separate index on <literal>y</literal> would serve reasonably well.  For
+   would be less useful for queries involving only <literal>y</literal>.  Just
+   how useful might depend on how effective the B-Tree index skip scan
+   optimization is; if <literal>x</literal> has no more than several hundred
+   distinct values, skip scan will make searches for specific
+   <literal>y</literal> values execute reasonably efficiently.  A combination
+   of a multicolumn index on <literal>(x, y)</literal> and a separate index on
+   <literal>y</literal> might also serve reasonably well.  For
    queries involving only <literal>x</literal>, the multicolumn index could be
    used, though it would be larger and hence slower than an index on
    <literal>x</literal> alone.  The last alternative is to create all three
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index a6d67d2fb..34deed8db 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4263,7 +4263,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage
      <replaceable>column_name</replaceable> =
      <replaceable>value2</replaceable> ...</literal> construct, though only
     when the optimizer transforms the construct into an equivalent
-    multi-valued array representation.
+    multi-valued array representation.  Similarly, when B-Tree index scans use
+    the skip scan strategy, an index search is performed each time the scan is
+    repositioned to the next index leaf page that might have matching tuples.
    </para>
   </note>
   <tip>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index 387baac7e..d0470ac79 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -860,6 +860,37 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE thousand IN (1, 2, 3, 4);
     <structname>tenk1_thous_tenthous</structname> index leaf page.
    </para>
 
+   <para>
+    The <quote>Index Searches</quote> line is also useful with B-tree index
+    scans that apply the <firstterm>skip scan</firstterm> optimization to
+    more efficiently traverse through an index:
+<screen>
+EXPLAIN ANALYZE SELECT four, unique1 FROM tenk1 WHERE four BETWEEN 1 AND 3 AND unique1 = 42;
+                                                              QUERY PLAN
+-------------------------------------------------------------------&zwsp;---------------------------------------------------------------
+ Index Only Scan using tenk1_four_unique1_idx on tenk1  (cost=0.29..6.90 rows=1 width=8) (actual time=0.006..0.007 rows=1.00 loops=1)
+   Index Cond: ((four &gt;= 1) AND (four &lt;= 3) AND (unique1 = 42))
+   Heap Fetches: 0
+   Index Searches: 3
+   Buffers: shared hit=7
+ Planning Time: 0.029 ms
+ Execution Time: 0.012 ms
+</screen>
+
+    Here we see an Index-Only Scan node using
+    <structname>tenk1_four_unique1_idx</structname>, a composite index on the
+    <structname>tenk1</structname> table's <structfield>four</structfield> and
+    <structfield>unique1</structfield> columns.  The scan performs 3 searches
+    that each read a single index leaf page:
+    <quote><literal>four = 1 AND unique1 = 42</literal></quote>,
+    <quote><literal>four = 2 AND unique1 = 42</literal></quote>, and
+    <quote><literal>four = 3 AND unique1 = 42</literal></quote>.  This index
+    is generally a good target for skip scan, since its leading column (the
+    <structfield>four</structfield> column) contains only 4 distinct values,
+    while its second/final column (the <structfield>unique1</structfield>
+    column) contains many distinct values.
+   </para>
+
    <para>
     Another type of extra information is the number of rows removed by a
     filter condition:
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 053619624..7e23a7b6e 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -461,6 +461,13 @@
        </entry>
        <entry>5</entry>
       </row>
+      <row>
+       <entry>
+        Return the addresses of C-callable skip support function(s)
+        (optional)
+       </entry>
+       <entry>6</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -1062,7 +1069,8 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint8cmp(int8, int8) ,
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1075,7 +1083,8 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint4cmp(int4, int4) ,
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1088,7 +1097,8 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 1 btint2cmp(int2, int2) ,
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
-  FUNCTION 4 btequalimage(oid) ;
+  FUNCTION 4 btequalimage(oid) ,
+  FUNCTION 6 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index 0c274d56a..23bf33f10 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 5
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
-ERROR:  invalid function number 6, must be between 1 and 5
+ERROR:  invalid function number 0, must be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -505,6 +505,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 ERROR:  ordering equal image functions must not be cross-type
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 8879554c3..bfb1a286e 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -581,6 +581,47 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Index Only Scan using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+ id 
+----
+ 55
+ 55
+(2 rows)
+
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Index Only Scan Backward using btree_tall_idx on btree_tall_tbl
+   Index Cond: (id = 55)
+(2 rows)
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 --
 -- Test for multilevel page deletion
 --
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 15be0043a..2cfb26699 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1637,7 +1637,9 @@ DROP TABLE syscol_table;
 -- Tests for IS NULL/IS NOT NULL with b-tree indexes
 --
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 SET enable_seqscan = OFF;
 SET enable_indexscan = ON;
@@ -1645,7 +1647,7 @@ SET enable_bitmapscan = ON;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1657,13 +1659,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1678,12 +1680,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1695,13 +1703,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1722,12 +1730,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0,
      1
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2 desc nulls last,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1739,13 +1753,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1760,12 +1774,18 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2  nulls first,unique1);
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL;
  count 
 -------
-     2
+     3
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
@@ -1777,13 +1797,13 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
  count 
 -------
-  1000
+  1002
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
  count 
 -------
-     1
+     2
 (1 row)
 
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
@@ -1798,6 +1818,12 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
      0
 (1 row)
 
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
+ unique1 | unique2 
+---------+---------
+     500 |        
+(1 row)
+
 DROP INDEX onek_nulltest;
 -- Check initial-positioning logic too
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2);
@@ -1829,20 +1855,24 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
 (2 rows)
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-         |        
-     278 |     999
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 5;
+ unique1 |  unique2   
+---------+------------
+     500 |           
+     100 |           
+         |           
+         | 2147483647
+     278 |        999
+(5 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
- unique1 | unique2 
----------+---------
-     278 |     999
-       0 |     998
-(2 rows)
+  ORDER BY unique2 DESC LIMIT 3;
+ unique1 |  unique2   
+---------+------------
+         | 2147483647
+     278 |        999
+       0 |        998
+(3 rows)
 
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
@@ -2247,7 +2277,8 @@ SELECT count(*) FROM dupindexcols
 (1 row)
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 explain (costs off)
 SELECT unique1 FROM tenk1
@@ -2269,7 +2300,7 @@ ORDER BY unique1;
       42
 (3 rows)
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2289,7 +2320,7 @@ ORDER BY thousand;
         1 |     1001
 (2 rows)
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -2309,6 +2340,25 @@ ORDER BY thousand DESC, tenthous DESC;
         0 |     3000
 (2 rows)
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ Index Only Scan Backward using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > 995) AND (tenthous = ANY ('{998,999}'::integer[])))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+ thousand | tenthous 
+----------+----------
+      999 |      999
+      998 |      998
+(2 rows)
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -2339,6 +2389,45 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 ---------
 (0 rows)
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+                   QUERY PLAN                    
+-------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY (NULL::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: (unique1 = ANY ('{NULL,NULL,NULL}'::integer[]))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+ unique1 
+---------
+(0 rows)
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Only Scan using tenk1_unique1 on tenk1
+   Index Cond: ((unique1 IS NULL) AND (unique1 IS NULL))
+(2 rows)
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+ unique1 
+---------
+(0 rows)
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
                                 QUERY PLAN                                 
@@ -2462,6 +2551,44 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 ---------
 (0 rows)
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand > '-1'::integer) AND (thousand >= 0) AND (tenthous = 3000))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        0 |     3000
+(1 row)
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Index Only Scan using tenk1_thous_tenthous on tenk1
+   Index Cond: ((thousand < 3) AND (thousand <= 2) AND (tenthous = 1001))
+(2 rows)
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+ thousand | tenthous 
+----------+----------
+        1 |     1001
+(1 row)
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index b1d12585e..cf48ae6d0 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5332,9 +5332,10 @@ Function              | in_range(time without time zone,time without time zone,i
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
+ btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(5 rows)
+(6 rows)
 
 -- check \dconfig
 set work_mem = 10240;
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d..5e20dc633 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -310,7 +310,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -444,6 +444,9 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree ADD
 -- Should fail. Not allowed to have cross-type equalimage function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
+-- Should fail. Not allowed to have cross-type skip support function.
+ALTER OPERATOR FAMILY alt_opf18 USING btree
+  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql
index 670ad5c6e..68c61dbc7 100644
--- a/src/test/regress/sql/btree_index.sql
+++ b/src/test/regress/sql/btree_index.sql
@@ -327,6 +327,27 @@ alter table btree_tall_tbl alter COLUMN t set storage plain;
 create index btree_tall_idx on btree_tall_tbl (t, id) with (fillfactor = 10);
 insert into btree_tall_tbl select g, repeat('x', 250)
 from generate_series(1, 130) g;
+insert into btree_tall_tbl select g, NULL
+from generate_series(50, 60) g;
+
+--
+-- Test for skip scan with type that lacks skip support (text)
+--
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+
+-- Forwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t, id;
+
+-- Backwards scan
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+explain (costs off)
+SELECT id FROM btree_tall_tbl WHERE id = 55 ORDER BY t DESC, id DESC;
+
+reset enable_seqscan;
+reset enable_bitmapscan;
 
 --
 -- Test for multilevel page deletion
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index 6b3852ddd..cd90b1c3a 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -650,7 +650,9 @@ DROP TABLE syscol_table;
 --
 
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
-INSERT INTO onek_with_null (unique1,unique2) VALUES (NULL, -1), (NULL, NULL);
+INSERT INTO onek_with_null(unique1, unique2)
+VALUES (NULL, -1), (NULL, 2_147_483_647), (NULL, NULL),
+       (100, NULL), (500, NULL);
 CREATE UNIQUE INDEX onek_nulltest ON onek_with_null (unique2,unique1);
 
 SET enable_seqscan = OFF;
@@ -663,6 +665,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -675,6 +678,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NUL
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IN (-1, 0, 1);
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -686,6 +690,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -697,6 +702,7 @@ SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique2 IS NOT NULL;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NOT NULL AND unique1 > 500;
 SELECT count(*) FROM onek_with_null WHERE unique1 IS NULL AND unique1 > 500;
+SELECT unique1, unique2 FROM onek_with_null WHERE unique1 = 500 ORDER BY unique2 DESC, unique1 DESC LIMIT 1;
 
 DROP INDEX onek_nulltest;
 
@@ -716,9 +722,9 @@ SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= 0
   ORDER BY unique2 LIMIT 2;
 
 SELECT unique1, unique2 FROM onek_with_null
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 5;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 >= -1
-  ORDER BY unique2 DESC LIMIT 2;
+  ORDER BY unique2 DESC LIMIT 3;
 SELECT unique1, unique2 FROM onek_with_null WHERE unique2 < 999
   ORDER BY unique2 DESC LIMIT 2;
 
@@ -852,7 +858,8 @@ SELECT count(*) FROM dupindexcols
   WHERE f1 BETWEEN 'WA' AND 'ZZZ' and id < 1000 and f1 ~<~ 'YX';
 
 --
--- Check that index scans with =ANY indexquals return rows in index order
+-- Check that index scans with SAOP array and/or skip array indexquals
+-- return rows in index order
 --
 
 explain (costs off)
@@ -864,7 +871,7 @@ SELECT unique1 FROM tenk1
 WHERE unique1 IN (1,42,7)
 ORDER BY unique1;
 
--- Non-required array scan key on "tenthous":
+-- Skip array on "thousand", SAOP array on "tenthous":
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -874,7 +881,7 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand;
 
--- Non-required array scan key on "tenthous", backward scan:
+-- Skip array on "thousand", SAOP array on "tenthous", backward scan:
 explain (costs off)
 SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
@@ -884,6 +891,15 @@ SELECT thousand, tenthous FROM tenk1
 WHERE thousand < 2 AND tenthous IN (1001,3000)
 ORDER BY thousand DESC, tenthous DESC;
 
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > 995 and tenthous in (998, 999)
+ORDER BY thousand desc;
+
 --
 -- Check elimination of redundant and contradictory index quals
 --
@@ -897,6 +913,21 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 
 SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('{33, 44}'::bigint[]);
 
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
+
+explain (costs off)
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
+SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL;
+
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
 
@@ -942,6 +973,26 @@ SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
 SELECT unique1 FROM tenk1 WHERE (thousand, tenthous) > (NULL, 5);
 
+-- Skip array redundancy (pair of redundant low_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand > -1 and thousand >= 0 AND tenthous = 3000
+ORDER BY thousand;
+
+-- Skip array redundancy (pair of redundant high_compare inequalities)
+explain (costs off)
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
+SELECT thousand, tenthous FROM tenk1
+WHERE thousand < 3 and thousand <= 2 AND tenthous = 1001
+ORDER BY thousand;
+
 --
 -- Check elimination of constant-NULL subexpressions
 --
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b66cecd87..4df54660c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -224,6 +224,7 @@ BTScanPos
 BTScanPosData
 BTScanPosItem
 BTShared
+BTSkipPreproc
 BTSortArrayContext
 BTSpool
 BTStack
@@ -2743,6 +2744,8 @@ SimpleStringListCell
 SingleBoundSortItem
 Size
 SkipPages
+SkipSupport
+SkipSupportData
 SlabBlock
 SlabContext
 SlabSlot
-- 
2.49.0

v33-0002-Enhance-nbtree-tuple-scan-key-optimizations.patchapplication/octet-stream; name=v33-0002-Enhance-nbtree-tuple-scan-key-optimizations.patchDownload
From b4007dfb2856a715a9fd0df31b9c74315aecf756 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 16 Nov 2024 15:58:41 -0500
Subject: [PATCH v33 2/5] Enhance nbtree tuple scan key optimizations.

Postgres 17 commit e0b1ee17 added two closely related nbtree
optimizations: the "prechecked" and "firstpage" optimizations.  Both
optimizations avoided needlessly evaluating keys that are guaranteed to
be satisfied by applying page-level context.  These optimizations were
adapted to work with the nbtree ScalarArrayOp execution patch a few
months later, which became commit 5bf748b8.

The "prechecked" design had a number of notable weak points.  It didn't
account for the fact that an = array scan key's sk_argument field might
need to advance at the point of the page precheck (it didn't check the
precheck tuple against the key's array, only the key's sk_argument,
which needlessly made it ineffective in corner cases involving stepping
to a page having advanced the scan's arrays using a truncated high key).
It was also an "all or nothing" optimization: either it was completely
effective (skipping all required-in-scan-direction keys against all
attributes) for the whole page, or it didn't work at all.  This also
implied that it couldn't be used on pages where the scan had to
terminate before reaching the end of the page due to an unsatisfied
low-order key setting continuescan=false.

Replace both optimizations with a new optimization without any of these
weak points.  This works by giving affected _bt_readpage calls a scankey
offset that its _bt_checkkeys calls start at (an offset to the first key
that might not be satisfied by every non-pivot tuple from the page).
The new optimization is activated at the same point as the previous
"prechecked" optimization (at the start of a _bt_readpage of any page
after the scan's first).

The old "prechecked" optimization worked off of the highest non-pivot
tuple on the page (or the lowest, when scanning backwards), but the new
"startikey" optimization always works off of a pair of non-pivot tuples
(the lowest and the highest, taken together).  This approach allows the
"startikey" optimization to bypass = array key comparisons whenever
they're satisfied by _some_ element (not necessarily the current one).
This is useful for SAOP array keys (it fixes the issue with truncated
high keys), and is needed to get the most out of range skip array keys
(we expect to be able to bypass range skip array = keys when a range of
values on the page all satisfy the key, even when there are multiple
values, provided they all "satisfy some range skip array element").

Although this is independently useful work, the main motivation is to
fix regressions in index scans that are nominally eligible to use skip
scan, but can never actually benefit from skipping.  These are cases
where a leading prefix column contains many distinct values, especially
when the number of values approaches the total number of index tuples,
where skipping can never be profitable.  The CPU costs of skip array
maintenance is by far the main cost that must be kept under control.

Skip scan's approach of adding skip arrays during preprocessing and then
fixing (or significantly ameliorating) the resulting regressions seen in
unsympathetic cases is enabled by the optimization added by this commit
(and by the "look ahead" optimization introduced by commit 5bf748b8).
This allows the planner to avoid generating distinct, competing index
paths (one path for skip scan, another for an equivalent traditional
full index scan).  The overall effect is to make scan runtime close to
optimal, even when the planner works off an incorrect cardinality
estimate.  Scans will also perform well given a skipped column with data
skew: individual groups of pages with many distinct values in respect of
a skipped column can be read about as efficiently as before, without
having to give up on skipping over other provably-irrelevant leaf pages.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reviewed-By: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Discussion: https://postgr.es/m/CAH2-Wz=Y93jf5WjoOsN=xvqpMjRy-bxCE037bVFi-EasrpeUJA@mail.gmail.com
Discussion: https://postgr.es/m/CAH2-WznWDK45JfNPNvDxh6RQy-TaCwULaM5u5ALMXbjLBMcugQ@mail.gmail.com
---
 src/include/access/nbtree.h                   |  11 +-
 src/backend/access/nbtree/nbtpreprocesskeys.c |   1 +
 src/backend/access/nbtree/nbtree.c            |   1 +
 src/backend/access/nbtree/nbtsearch.c         |  77 +--
 src/backend/access/nbtree/nbtutils.c          | 543 ++++++++++++++----
 5 files changed, 479 insertions(+), 154 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index b86bf7bf3..c8708f2fd 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1059,6 +1059,7 @@ typedef struct BTScanOpaqueData
 
 	/* workspace for SK_SEARCHARRAY support */
 	int			numArrayKeys;	/* number of equality-type array keys */
+	bool		skipScan;		/* At least one skip array in arrayKeys[]? */
 	bool		needPrimScan;	/* New prim scan to continue in current dir? */
 	bool		scanBehind;		/* Check scan not still behind on next page? */
 	bool		oppositeDirCheck;	/* scanBehind opposite-scan-dir check? */
@@ -1105,6 +1106,8 @@ typedef struct BTReadPageState
 	IndexTuple	finaltup;		/* Needed by scans with array keys */
 	Page		page;			/* Page being read */
 	bool		firstpage;		/* page is first for primitive scan? */
+	bool		forcenonrequired;	/* treat all keys as nonrequired? */
+	int			startikey;		/* start comparisons from this scan key */
 
 	/* Per-tuple input parameters, set by _bt_readpage for _bt_checkkeys */
 	OffsetNumber offnum;		/* current tuple's page offset number */
@@ -1113,13 +1116,6 @@ typedef struct BTReadPageState
 	OffsetNumber skip;			/* Array keys "look ahead" skip offnum */
 	bool		continuescan;	/* Terminate ongoing (primitive) index scan? */
 
-	/*
-	 * Input and output parameters, set and unset by both _bt_readpage and
-	 * _bt_checkkeys to manage precheck optimizations
-	 */
-	bool		prechecked;		/* precheck set continuescan to 'true'? */
-	bool		firstmatch;		/* at least one match so far?  */
-
 	/*
 	 * Private _bt_checkkeys state used to manage "look ahead" optimization
 	 * (only used during scans with array keys)
@@ -1327,6 +1323,7 @@ extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arra
 						  IndexTuple tuple, int tupnatts);
 extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
 									 IndexTuple finaltup);
+extern void _bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate);
 extern void _bt_killitems(IndexScanDesc scan);
 extern BTCycleId _bt_vacuum_cycleid(Relation rel);
 extern BTCycleId _bt_start_vacuum(Relation rel);
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 5c08cda25..339092dfa 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -1389,6 +1389,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
+	so->skipScan = (numSkipArrayKeys > 0);
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index bdadbf73c..325804ae7 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -349,6 +349,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys)
 	else
 		so->keyData = NULL;
 
+	so->skipScan = false;
 	so->needPrimScan = false;
 	so->scanBehind = false;
 	so->oppositeDirCheck = false;
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 1ef2cb2b5..e95c396d2 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1648,47 +1648,14 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 	pstate.finaltup = NULL;
 	pstate.page = page;
 	pstate.firstpage = firstpage;
+	pstate.forcenonrequired = false;
+	pstate.startikey = 0;
 	pstate.offnum = InvalidOffsetNumber;
 	pstate.skip = InvalidOffsetNumber;
 	pstate.continuescan = true; /* default assumption */
-	pstate.prechecked = false;
-	pstate.firstmatch = false;
 	pstate.rechecks = 0;
 	pstate.targetdistance = 0;
 
-	/*
-	 * Prechecking the value of the continuescan flag for the last item on the
-	 * page (for backwards scan it will be the first item on a page).  If we
-	 * observe it to be true, then it should be true for all other items. This
-	 * allows us to do significant optimizations in the _bt_checkkeys()
-	 * function for all the items on the page.
-	 *
-	 * With the forward scan, we do this check for the last item on the page
-	 * instead of the high key.  It's relatively likely that the most
-	 * significant column in the high key will be different from the
-	 * corresponding value from the last item on the page.  So checking with
-	 * the last item on the page would give a more precise answer.
-	 *
-	 * We skip this for the first page read by each (primitive) scan, to avoid
-	 * slowing down point queries.  They typically don't stand to gain much
-	 * when the optimization can be applied, and are more likely to notice the
-	 * overhead of the precheck.  Also avoid it during scans with array keys,
-	 * which might be using skip scan (XXX fixed in next commit).
-	 */
-	if (!pstate.firstpage && !arrayKeys && minoff < maxoff)
-	{
-		ItemId		iid;
-		IndexTuple	itup;
-
-		iid = PageGetItemId(page, ScanDirectionIsForward(dir) ? maxoff : minoff);
-		itup = (IndexTuple) PageGetItem(page, iid);
-
-		/* Call with arrayKeys=false to avoid undesirable side-effects */
-		_bt_checkkeys(scan, &pstate, false, itup, indnatts);
-		pstate.prechecked = pstate.continuescan;
-		pstate.continuescan = true; /* reset */
-	}
-
 	if (ScanDirectionIsForward(dir))
 	{
 		/* SK_SEARCHARRAY forward scans must provide high key up front */
@@ -1716,6 +1683,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
+		/*
+		 * Consider pstate.startikey optimization once the ongoing primitive
+		 * index scan has already read at least one page
+		 */
+		if (!pstate.firstpage && minoff < maxoff)
+			_bt_set_startikey(scan, &pstate);
+
 		/* load items[] in ascending order */
 		itemIndex = 0;
 
@@ -1752,6 +1726,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum < pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1761,7 +1736,6 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			if (passes_quals)
 			{
 				/* tuple passes all scan key conditions */
-				pstate.firstmatch = true;
 				if (!BTreeTupleIsPosting(itup))
 				{
 					/* Remember it */
@@ -1816,7 +1790,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			int			truncatt;
 
 			truncatt = BTreeTupleGetNAtts(itup, rel);
-			pstate.prechecked = false;	/* precheck didn't cover HIKEY */
+			pstate.forcenonrequired = false;
+			pstate.startikey = 0;
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -1855,6 +1830,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			so->scanBehind = so->oppositeDirCheck = false;	/* reset */
 		}
 
+		/*
+		 * Consider pstate.startikey optimization once the ongoing primitive
+		 * index scan has already read at least one page
+		 */
+		if (!pstate.firstpage && minoff < maxoff)
+			_bt_set_startikey(scan, &pstate);
+
 		/* load items[] in descending order */
 		itemIndex = MaxTIDsPerBTreePage;
 
@@ -1894,6 +1876,11 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			Assert(!BTreeTupleIsPivot(itup));
 
 			pstate.offnum = offnum;
+			if (arrayKeys && offnum == minoff && pstate.forcenonrequired)
+			{
+				pstate.forcenonrequired = false;
+				pstate.startikey = 0;
+			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
 
@@ -1905,6 +1892,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				Assert(!passes_quals && pstate.continuescan);
 				Assert(offnum > pstate.skip);
+				Assert(!pstate.forcenonrequired);
 
 				offnum = pstate.skip;
 				pstate.skip = InvalidOffsetNumber;
@@ -1914,7 +1902,6 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			if (passes_quals && tuple_alive)
 			{
 				/* tuple passes all scan key conditions */
-				pstate.firstmatch = true;
 				if (!BTreeTupleIsPosting(itup))
 				{
 					/* Remember it */
@@ -1970,6 +1957,20 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 		so->currPos.itemIndex = MaxTIDsPerBTreePage - 1;
 	}
 
+	/*
+	 * If _bt_set_startikey told us to temporarily treat the scan's keys as
+	 * nonrequired (possible only during scans with array keys), there must be
+	 * no lasting consequences for the scan's array keys.  The scan's arrays
+	 * should now have exactly the same elements as they would have had if the
+	 * nonrequired behavior had never been used.  (In general, a scan's arrays
+	 * are expected to track its progress through the index's key space.)
+	 *
+	 * We are required (by _bt_set_startikey) to call _bt_checkkeys against
+	 * pstate.finaltup with pstate.forcenonrequired=false to allow the scan's
+	 * arrays to recover.  Assert that that step hasn't been missed.
+	 */
+	Assert(!pstate.forcenonrequired);
+
 	return (so->currPos.firstItem <= so->currPos.lastItem);
 }
 
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 108030a8e..ea5b3b688 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -57,11 +57,11 @@ static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 								  IndexTuple finaltup);
 static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 							  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-							  bool advancenonrequired, bool prechecked, bool firstmatch,
+							  bool advancenonrequired, bool forcenonrequired,
 							  bool *continuescan, int *ikey);
 static bool _bt_check_rowcompare(ScanKey skey,
 								 IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-								 ScanDirection dir, bool *continuescan);
+								 ScanDirection dir, bool forcenonrequired, bool *continuescan);
 static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 									 int tupnatts, TupleDesc tupdesc);
 static int	_bt_keep_natts(Relation rel, IndexTuple lastleft,
@@ -1422,9 +1422,10 @@ _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir)
  * postcondition's <= operator with a >=.  In other words, just swap the
  * precondition with the postcondition.)
  *
- * We also deal with "advancing" non-required arrays here.  Callers whose
- * sktrig scan key is non-required specify sktrig_required=false.  These calls
- * are the only exception to the general rule about always advancing the
+ * We also deal with "advancing" non-required arrays here (or arrays that are
+ * treated as non-required for the duration of a _bt_readpage call).  Callers
+ * whose sktrig scan key is non-required specify sktrig_required=false.  These
+ * calls are the only exception to the general rule about always advancing the
  * required array keys (the scan may not even have a required array).  These
  * callers should just pass a NULL pstate (since there is never any question
  * of stopping the scan).  No call to _bt_tuple_before_array_skeys is required
@@ -1464,6 +1465,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 				all_satisfied = true;
 
 	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
+	Assert(_bt_verify_keys_with_arraykeys(scan));
 
 	if (sktrig_required)
 	{
@@ -1473,17 +1475,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
 											 tupnatts, false, 0, NULL));
 
-		/*
-		 * Required scan key wasn't satisfied, so required arrays will have to
-		 * advance.  Invalidate page-level state that tracks whether the
-		 * scan's required-in-opposite-direction-only keys are known to be
-		 * satisfied by page's remaining tuples.
-		 */
-		pstate->firstmatch = false;
-
-		/* Shouldn't have to invalidate 'prechecked', though */
-		Assert(!pstate->prechecked);
-
 		/*
 		 * Once we return we'll have a new set of required array keys, so
 		 * reset state used by "look ahead" optimization
@@ -1491,8 +1482,26 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		pstate->rechecks = 0;
 		pstate->targetdistance = 0;
 	}
+	else if (sktrig < so->numberOfKeys - 1 &&
+			 !(so->keyData[so->numberOfKeys - 1].sk_flags & SK_SEARCHARRAY))
+	{
+		int			least_sign_ikey = so->numberOfKeys - 1;
+		bool		continuescan;
 
-	Assert(_bt_verify_keys_with_arraykeys(scan));
+		/*
+		 * Optimization: perform a precheck of the least significant key
+		 * during !sktrig_required calls when it isn't already our sktrig
+		 * (provided the precheck key is not itself an array).
+		 *
+		 * When the precheck works out we'll avoid an expensive binary search
+		 * of sktrig's array (plus any other arrays before least_sign_ikey).
+		 */
+		Assert(so->keyData[sktrig].sk_flags & SK_SEARCHARRAY);
+		if (!_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false,
+							   false, &continuescan,
+							   &least_sign_ikey))
+			return false;
+	}
 
 	for (int ikey = 0; ikey < so->numberOfKeys; ikey++)
 	{
@@ -1534,8 +1543,6 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))
 		{
-			Assert(sktrig_required);
-
 			required = true;
 
 			if (cur->sk_attno > tupnatts)
@@ -1669,7 +1676,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		}
 		else
 		{
-			Assert(sktrig_required && required);
+			Assert(required);
 
 			/*
 			 * This is a required non-array equality strategy scan key, which
@@ -1711,7 +1718,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		 * be eliminated by _bt_preprocess_keys.  It won't matter if some of
 		 * our "true" array scan keys (or even all of them) are non-required.
 		 */
-		if (required &&
+		if (sktrig_required && required &&
 			((ScanDirectionIsForward(dir) && result > 0) ||
 			 (ScanDirectionIsBackward(dir) && result < 0)))
 			beyond_end_advance = true;
@@ -1726,7 +1733,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 			 * array scan keys are considered interesting.)
 			 */
 			all_satisfied = false;
-			if (required)
+			if (sktrig_required && required)
 				all_required_satisfied = false;
 			else
 			{
@@ -1786,6 +1793,12 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 	 * of any required scan key).  All that matters is whether caller's tuple
 	 * satisfies the new qual, so it's safe to just skip the _bt_check_compare
 	 * recheck when we've already determined that it can only return 'false'.
+	 *
+	 * Note: In practice most scan keys are marked required by preprocessing,
+	 * if necessary by generating a preceding skip array.  We nevertheless
+	 * often handle array keys marked required as if they were nonrequired.
+	 * This behavior is requested by our _bt_check_compare caller, though only
+	 * when it is passed "forcenonrequired=true" by _bt_checkkeys.
 	 */
 	if ((sktrig_required && all_required_satisfied) ||
 		(!sktrig_required && all_satisfied))
@@ -1796,9 +1809,9 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 		Assert(all_required_satisfied);
 
 		/* Recheck _bt_check_compare on behalf of caller */
-		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							  false, false, false,
-							  &continuescan, &nsktrig) &&
+		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false,
+							  false, &continuescan,
+							  &nsktrig) &&
 			!so->scanBehind)
 		{
 			/* This tuple satisfies the new qual */
@@ -2042,8 +2055,9 @@ new_prim_scan:
 	 * read at least one leaf page before the one we're reading now.  This
 	 * makes primscan scheduling more efficient when scanning subsets of an
 	 * index with many distinct attribute values matching many array elements.
-	 * It encourages fewer, larger primitive scans where that makes sense
-	 * (where index descent costs need to be kept under control).
+	 * It encourages fewer, larger primitive scans where that makes sense.
+	 * This will in turn encourage _bt_readpage to apply the pstate.startikey
+	 * optimization more often.
 	 *
 	 * Note: This heuristic isn't as aggressive as you might think.  We're
 	 * conservative about allowing a primitive scan to step from the first
@@ -2200,17 +2214,14 @@ _bt_verify_keys_with_arraykeys(IndexScanDesc scan)
  * the page to the right.
  *
  * Advances the scan's array keys when necessary for arrayKeys=true callers.
- * Caller can avoid all array related side-effects when calling just to do a
- * page continuescan precheck -- pass arrayKeys=false for that.  Scans without
- * any arrays keys must always pass arrayKeys=false.
+ * Scans without any array keys must always pass arrayKeys=false.
  *
  * Also stops and starts primitive index scans for arrayKeys=true callers.
  * Scans with array keys are required to set up page state that helps us with
  * this.  The page's finaltup tuple (the page high key for a forward scan, or
  * the page's first non-pivot tuple for a backward scan) must be set in
- * pstate.finaltup ahead of the first call here for the page (or possibly the
- * first call after an initial continuescan-setting page precheck call).  Set
- * this to NULL for rightmost page (or the leftmost page for backwards scans).
+ * pstate.finaltup ahead of the first call here for the page.  Set this to
+ * NULL for rightmost page (or the leftmost page for backwards scans).
  *
  * scan: index scan descriptor (containing a search-type scankey)
  * pstate: page level input and output parameters
@@ -2225,42 +2236,48 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	TupleDesc	tupdesc = RelationGetDescr(scan->indexRelation);
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	ScanDirection dir = so->currPos.dir;
-	int			ikey = 0;
+	int			ikey = pstate->startikey;
 	bool		res;
 
 	Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts);
 	Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck);
+	Assert(arrayKeys || so->numArrayKeys == 0);
 
-	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-							arrayKeys, pstate->prechecked, pstate->firstmatch,
-							&pstate->continuescan, &ikey);
+	res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, arrayKeys,
+							pstate->forcenonrequired, &pstate->continuescan,
+							&ikey);
 
+	/*
+	 * If _bt_check_compare relied on the pstate.startikey optimization, call
+	 * again (in assert-enabled builds) to verify it didn't affect our answer.
+	 *
+	 * Note: we can't do this when !pstate.forcenonrequired, since any arrays
+	 * before pstate.startikey won't have advanced on this page at all.
+	 */
+	Assert(!pstate->forcenonrequired || arrayKeys);
 #ifdef USE_ASSERT_CHECKING
-	if (!arrayKeys && so->numArrayKeys)
+	if (pstate->startikey > 0 && !pstate->forcenonrequired)
 	{
-		/*
-		 * This is a continuescan precheck call for a scan with array keys.
-		 *
-		 * Assert that the scan isn't in danger of becoming confused.
-		 */
-		Assert(!so->scanBehind && !so->oppositeDirCheck);
-		Assert(!pstate->prechecked && !pstate->firstmatch);
-		Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc,
-											 tupnatts, false, 0, NULL));
-	}
-	if (pstate->prechecked || pstate->firstmatch)
-	{
-		bool		dcontinuescan;
+		bool		dres,
+					dcontinuescan;
 		int			dikey = 0;
 
-		/*
-		 * Call relied on continuescan/firstmatch prechecks -- assert that we
-		 * get the same answer without those optimizations
-		 */
-		Assert(res == _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc,
-										false, false, false,
-										&dcontinuescan, &dikey));
+		/* Pass arrayKeys=false to avoid array side-effects */
+		dres = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false,
+								 pstate->forcenonrequired, &dcontinuescan,
+								 &dikey);
+		Assert(res == dres);
 		Assert(pstate->continuescan == dcontinuescan);
+
+		/*
+		 * Should also get the same ikey result.  We need a slightly weaker
+		 * assertion during arrayKeys calls, since they might be using an
+		 * array that couldn't be marked required during preprocessing
+		 * (preprocessing occasionally fails to add a "bridging" skip array,
+		 * due to implementation restrictions around RowCompare keys).
+		 */
+		Assert(arrayKeys || ikey == dikey);
+		Assert(ikey <= dikey);
 	}
 #endif
 
@@ -2281,6 +2298,7 @@ _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys,
 	 * It's also possible that the scan is still _before_ the _start_ of
 	 * tuples matching the current set of array keys.  Check for that first.
 	 */
+	Assert(!pstate->forcenonrequired);
 	if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true,
 									 ikey, NULL))
 	{
@@ -2394,8 +2412,9 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 
 	Assert(so->numArrayKeys);
 
-	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc,
-					  false, false, false, &continuescan, &ikey);
+	_bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc, false,
+					  false, &continuescan,
+					  &ikey);
 
 	if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber)
 		return false;
@@ -2403,6 +2422,294 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	return true;
 }
 
+/*
+ * Determines an offset to the first scan key (an so->keyData[]-wise offset)
+ * that is _not_ guaranteed to be satisfied by every tuple from pstate.page,
+ * which is set in pstate.startikey for _bt_checkkeys calls for the page.
+ * This allows caller to save cycles on comparisons of a prefix of keys while
+ * reading pstate.page.
+ *
+ * Also determines if later calls to _bt_checkkeys (for pstate.page) should be
+ * forced to treat all required scan keys >= pstate.startikey as nonrequired
+ * (that is, if they're to be treated as if any SK_BT_REQFWD/SK_BT_REQBKWD
+ * markings that were set by preprocessing were not set at all, for the
+ * duration of _bt_checkkeys calls prior to the call for pstate.finaltup).
+ * This is indicated to caller by setting pstate.forcenonrequired.
+ *
+ * Call here at the start of reading a leaf page beyond the first one for the
+ * primitive index scan.  We consider all non-pivot tuples, so it doesn't make
+ * sense to call here when only a subset of those tuples can ever be read.
+ * This is also a good idea on performance grounds; not calling here when on
+ * the first page (first for the current primitive scan) avoids wasting cycles
+ * during selective point queries.  They typically don't stand to gain as much
+ * when we can set pstate.startikey, and are likely to notice the overhead of
+ * calling here.
+ *
+ * Caller must reset startikey and forcenonrequired ahead of the _bt_checkkeys
+ * call for pstate.finaltup iff we set forcenonrequired=true.  This will give
+ * _bt_checkkeys the opportunity to call _bt_advance_array_keys once more,
+ * with sktrig_required=true, to advance the arrays that were ignored during
+ * checks of all of the page's prior tuples.  Caller doesn't need to do this
+ * on the rightmost/leftmost page in the index (where pstate.finaltup isn't
+ * set), since forcenonrequired won't be set here by us in the first place.
+ */
+void
+_bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	Relation	rel = scan->indexRelation;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	ItemId		iid;
+	IndexTuple	firsttup,
+				lasttup;
+	int			startikey = 0,
+				arrayidx = 0,
+				firstchangingattnum;
+	bool		start_past_saop_eq = false;
+
+	Assert(!so->scanBehind);
+	Assert(pstate->minoff < pstate->maxoff);
+	Assert(!pstate->firstpage);
+	Assert(pstate->startikey == 0);
+
+	if (so->numberOfKeys == 0)
+		return;
+
+	/* minoff is an offset to the lowest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->minoff);
+	firsttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* maxoff is an offset to the highest non-pivot tuple on the page */
+	iid = PageGetItemId(pstate->page, pstate->maxoff);
+	lasttup = (IndexTuple) PageGetItem(pstate->page, iid);
+
+	/* Determine the first attribute whose values change on caller's page */
+	firstchangingattnum = _bt_keep_natts_fast(rel, firsttup, lasttup);
+
+	for (; startikey < so->numberOfKeys; startikey++)
+	{
+		ScanKey		key = so->keyData + startikey;
+		BTArrayKeyInfo *array;
+		Datum		firstdatum,
+					lastdatum;
+		bool		firstnull,
+					lastnull;
+		int32		result;
+
+		/*
+		 * Determine if it's safe to set pstate.startikey to an offset to a
+		 * key that comes after this key, by examining this key
+		 */
+		if (unlikely(!(key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))))
+		{
+			/* Scan key isn't marked required (corner case) */
+			Assert(!(key->sk_flags & SK_ROW_HEADER));
+			break;				/* unsafe */
+		}
+		if (key->sk_flags & SK_ROW_HEADER)
+		{
+			/*
+			 * Can't let pstate.startikey get set to an ikey beyond a
+			 * RowCompare inequality
+			 */
+			break;				/* unsafe */
+		}
+		if (key->sk_strategy != BTEqualStrategyNumber)
+		{
+			/*
+			 * Scalar inequality key.
+			 *
+			 * It's definitely safe for _bt_checkkeys to avoid assessing this
+			 * inequality when the page's first and last non-pivot tuples both
+			 * satisfy the inequality (since the same must also be true of all
+			 * the tuples in between these two).
+			 *
+			 * Unlike the "=" case, it doesn't matter if this attribute has
+			 * more than one distinct value (though it _is_ necessary for any
+			 * and all _prior_ attributes to contain no more than one distinct
+			 * value amongst all of the tuples from pstate.page).
+			 */
+			if (key->sk_attno > firstchangingattnum)	/* >, not >= */
+				break;			/* unsafe, preceding attr has multiple
+								 * distinct values */
+
+			firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &firstnull);
+			lastdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &lastnull);
+
+			if (key->sk_flags & SK_ISNULL)
+			{
+				/* IS NOT NULL key */
+				Assert(key->sk_flags & SK_SEARCHNOTNULL);
+
+				if (firstnull || lastnull)
+					break;		/* unsafe */
+
+				/* Safe, IS NOT NULL key satisfied by every tuple */
+				continue;
+			}
+
+			/* Test firsttup */
+			if (firstnull ||
+				!DatumGetBool(FunctionCall2Coll(&key->sk_func,
+												key->sk_collation, firstdatum,
+												key->sk_argument)))
+				break;			/* unsafe */
+
+			/* Test lasttup */
+			if (lastnull ||
+				!DatumGetBool(FunctionCall2Coll(&key->sk_func,
+												key->sk_collation, lastdatum,
+												key->sk_argument)))
+				break;			/* unsafe */
+
+			/* Safe, scalar inequality satisfied by every tuple */
+			continue;
+		}
+
+		/* Some = key (could be a a scalar = key, could be an array = key) */
+		Assert(key->sk_strategy == BTEqualStrategyNumber);
+
+		if (!(key->sk_flags & SK_SEARCHARRAY))
+		{
+			/*
+			 * Scalar = key (posibly an IS NULL key).
+			 *
+			 * It is unsafe to set pstate.startikey to an ikey beyond this
+			 * key, unless the = key is satisfied by every possible tuple on
+			 * the page (possible only when attribute has just one distinct
+			 * value among all tuples on the page).
+			 */
+			if (key->sk_attno >= firstchangingattnum)
+				break;			/* unsafe, multiple distinct attr values */
+
+			firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+									   &firstnull);
+			if (key->sk_flags & SK_ISNULL)
+			{
+				/* IS NULL key */
+				Assert(key->sk_flags & SK_SEARCHNULL);
+
+				if (!firstnull)
+					break;		/* unsafe */
+
+				/* Safe, IS NULL key satisfied by every tuple */
+				continue;
+			}
+			if (firstnull ||
+				!DatumGetBool(FunctionCall2Coll(&key->sk_func,
+												key->sk_collation, firstdatum,
+												key->sk_argument)))
+				break;			/* unsafe */
+
+			/* Safe, scalar = key satisfied by every tuple */
+			continue;
+		}
+
+		/* = array key (could be a SAOP array, could be a skip array) */
+		array = &so->arrayKeys[arrayidx++];
+		Assert(array->scan_key == startikey);
+		if (array->num_elems != -1)
+		{
+			/*
+			 * SAOP array = key.
+			 *
+			 * Handle this like we handle scalar = keys (though binary search
+			 * for a matching element, to avoid relying on key's sk_argument).
+			 */
+			if (key->sk_attno >= firstchangingattnum)
+				break;			/* unsafe, multiple distinct attr values */
+
+			firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
+									   &firstnull);
+			_bt_binsrch_array_skey(&so->orderProcs[startikey],
+								   false, NoMovementScanDirection,
+								   firstdatum, firstnull, array, key,
+								   &result);
+			if (result != 0)
+				break;			/* unsafe */
+
+			/* Safe, SAOP = key satisfied by every tuple */
+			start_past_saop_eq = true;
+			continue;
+		}
+
+		/*
+		 * Skip array = key.
+		 *
+		 * Handle this like we handle scalar inequality keys (but avoid using
+		 * key's sk_argument/advancing array, as in the SAOP array case).
+		 */
+		if (array->null_elem)
+		{
+			/*
+			 * Safe, non-range skip array "satisfied" by every tuple on page
+			 * (safe even when "key->sk_attno <= firstchangingattnum")
+			 */
+			continue;
+		}
+		else if (key->sk_attno > firstchangingattnum)	/* >, not >= */
+		{
+			break;				/* unsafe, preceding attr has multiple
+								 * distinct values */
+		}
+
+		firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &firstnull);
+		lastdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &lastnull);
+
+		/* Test firsttup */
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   firstdatum, firstnull, array, key,
+								   &result);
+		if (result != 0)
+			break;				/* unsafe */
+
+		/* Test lasttup */
+		_bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+								   lastdatum, lastnull, array, key,
+								   &result);
+		if (result != 0)
+			break;				/* unsafe */
+
+		/* Safe, range skip array satisfied by every tuple */
+	}
+
+	/*
+	 * Use of forcenonrequired is typically undesirable, since it'll force
+	 * _bt_readpage caller to read every tuple on the page -- even though, in
+	 * general, it might well be possible to end the scan on an earlier tuple.
+	 * However, caller must use forcenonrequired when start_past_saop_eq=true,
+	 * since the usual required array behavior might fail to roll over to the
+	 * SAOP array.  This is no loss, since it can only happen when reading
+	 * pages that must have all their tuples read either way.
+	 *
+	 * We always prefer forcenonrequired=true during scans with skip arrays
+	 * (except on the first page of each primitive index scan), though -- even
+	 * when "startikey == 0".  That way, _bt_advance_array_keys's low-order
+	 * key precheck optimization can always be used (unless on the first page
+	 * of the scan).  It seems slightly preferable to check more tuples when
+	 * that allows us to do significantly less skip array maintenance.
+	 */
+	pstate->forcenonrequired = (start_past_saop_eq || so->skipScan);
+	pstate->startikey = startikey;
+
+	/*
+	 * _bt_readpage caller is required to call _bt_checkkeys against page's
+	 * finaltup with forcenonrequired=false whenever we initially set
+	 * forcenonrequired=true.  That way the scan's arrays will reliably track
+	 * its progress through the index's key space.
+	 *
+	 * We don't expect this when _bt_readpage caller has no finaltup due to
+	 * its page being the rightmost (or the leftmost, during backwards scans).
+	 * When we see that _bt_readpage has no finaltup, back out of everything.
+	 */
+	Assert(!pstate->forcenonrequired || so->numArrayKeys);
+	if (pstate->forcenonrequired && !pstate->finaltup)
+	{
+		pstate->forcenonrequired = false;
+		pstate->startikey = 0;
+	}
+}
+
 /*
  * Test whether an indextuple satisfies current scan condition.
  *
@@ -2432,23 +2739,33 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  * by the current array key, or if they're truly unsatisfied (that is, if
  * they're unsatisfied by every possible array key).
  *
- * Though we advance non-required array keys on our own, that shouldn't have
- * any lasting consequences for the scan.  By definition, non-required arrays
- * have no fixed relationship with the scan's progress.  (There are delicate
- * considerations for non-required arrays when the arrays need to be advanced
- * following our setting continuescan to false, but that doesn't concern us.)
- *
  * Pass advancenonrequired=false to avoid all array related side effects.
  * This allows _bt_advance_array_keys caller to avoid infinite recursion.
+ *
+ * Pass forcenonrequired=true to instruct us to treat all keys as nonrequired.
+ * This is used to make it safe to temporarily stop properly maintaining the
+ * scan's required arrays.  _bt_checkkeys caller (_bt_readpage, actually)
+ * determines a prefix of keys that must satisfy every possible corresponding
+ * index attribute value from its page, which is passed to us via *ikey arg
+ * (this is the first key that might be unsatisfied by tuples on the page).
+ * Obviously, we won't maintain any array keys from before *ikey, so it's
+ * quite possible for such arrays to "fall behind" the index's keyspace.
+ * Caller will need to "catch up" by passing forcenonrequired=true (alongside
+ * an *ikey=0) once the page's finaltup is reached.
+ *
+ * Note: it's safe to pass an *ikey > 0 with forcenonrequired=false, but only
+ * when caller determines that it won't affect array maintenance.
  */
 static bool
 _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				  IndexTuple tuple, int tupnatts, TupleDesc tupdesc,
-				  bool advancenonrequired, bool prechecked, bool firstmatch,
+				  bool advancenonrequired, bool forcenonrequired,
 				  bool *continuescan, int *ikey)
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
+	Assert(!forcenonrequired || advancenonrequired);
+
 	*continuescan = true;		/* default assumption */
 
 	for (; *ikey < so->numberOfKeys; (*ikey)++)
@@ -2461,36 +2778,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 
 		/*
 		 * Check if the key is required in the current scan direction, in the
-		 * opposite scan direction _only_, or in neither direction
+		 * opposite scan direction _only_, or in neither direction (except
+		 * when we're forced to treat all scan keys as nonrequired)
 		 */
-		if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
-			((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
+		if (forcenonrequired)
+		{
+			/* treating scan's keys as non-required */
+		}
+		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) ||
+				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir)))
 			requiredSameDir = true;
 		else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) ||
 				 ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir)))
 			requiredOppositeDirOnly = true;
 
-		/*
-		 * If the caller told us the *continuescan flag is known to be true
-		 * for the last item on the page, then we know the keys required for
-		 * the current direction scan should be matched.  Otherwise, the
-		 * *continuescan flag would be set for the current item and
-		 * subsequently the last item on the page accordingly.
-		 *
-		 * If the key is required for the opposite direction scan, we can skip
-		 * the check if the caller tells us there was already at least one
-		 * matching item on the page. Also, we require the *continuescan flag
-		 * to be true for the last item on the page to know there are no
-		 * NULLs.
-		 *
-		 * Both cases above work except for the row keys, where NULLs could be
-		 * found in the middle of matching values.
-		 */
-		if (prechecked &&
-			(requiredSameDir || (requiredOppositeDirOnly && firstmatch)) &&
-			!(key->sk_flags & SK_ROW_HEADER))
-			continue;
-
 		if (key->sk_attno > tupnatts)
 		{
 			/*
@@ -2512,6 +2813,16 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		{
 			Assert(key->sk_flags & SK_SEARCHARRAY);
 			Assert(key->sk_flags & SK_BT_SKIP);
+			Assert(requiredSameDir || forcenonrequired);
+
+			/*
+			 * Cannot fall back on _bt_tuple_before_array_skeys when we're
+			 * treating the scan's keys as nonrequired, though.  Just handle
+			 * this like any other non-required equality-type array key.
+			 */
+			if (forcenonrequired)
+				return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
+											  tupdesc, *ikey, false);
 
 			*continuescan = false;
 			return false;
@@ -2521,7 +2832,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir,
-									 continuescan))
+									 forcenonrequired, continuescan))
 				continue;
 			return false;
 		}
@@ -2554,9 +2865,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			 */
 			if (requiredSameDir)
 				*continuescan = false;
+			else if (unlikely(key->sk_flags & SK_BT_SKIP))
+			{
+				/*
+				 * If we're treating scan keys as nonrequired, and encounter a
+				 * skip array scan key whose current element is NULL, then it
+				 * must be a non-range skip array
+				 */
+				Assert(forcenonrequired && *ikey > 0);
+				return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
+											  tupdesc, *ikey, false);
+			}
 
 			/*
-			 * In any case, this indextuple doesn't match the qual.
+			 * This indextuple doesn't match the qual.
 			 */
 			return false;
 		}
@@ -2577,7 +2899,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * forward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsBackward(dir))
 					*continuescan = false;
 			}
@@ -2595,7 +2917,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 				 * (_bt_advance_array_keys also relies on this behavior during
 				 * backward scans.)
 				 */
-				if ((key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
+				if ((requiredSameDir || requiredOppositeDirOnly) &&
 					ScanDirectionIsForward(dir))
 					*continuescan = false;
 			}
@@ -2606,15 +2928,7 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 			return false;
 		}
 
-		/*
-		 * Apply the key-checking function, though only if we must.
-		 *
-		 * When a key is required in the opposite-of-scan direction _only_,
-		 * then it must already be satisfied if firstmatch=true indicates that
-		 * an earlier tuple from this same page satisfied it earlier on.
-		 */
-		if (!(requiredOppositeDirOnly && firstmatch) &&
-			!DatumGetBool(FunctionCall2Coll(&key->sk_func, key->sk_collation,
+		if (!DatumGetBool(FunctionCall2Coll(&key->sk_func, key->sk_collation,
 											datum, key->sk_argument)))
 		{
 			/*
@@ -2664,7 +2978,8 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
  */
 static bool
 _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
-					 TupleDesc tupdesc, ScanDirection dir, bool *continuescan)
+					 TupleDesc tupdesc, ScanDirection dir,
+					 bool forcenonrequired, bool *continuescan)
 {
 	ScanKey		subkey = (ScanKey) DatumGetPointer(skey->sk_argument);
 	int32		cmpresult = 0;
@@ -2704,7 +3019,11 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 
 		if (isNull)
 		{
-			if (subkey->sk_flags & SK_BT_NULLS_FIRST)
+			if (forcenonrequired)
+			{
+				/* treating scan's keys as non-required */
+			}
+			else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
 			{
 				/*
 				 * Since NULLs are sorted before non-NULLs, we know we have
@@ -2758,8 +3077,12 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			 */
 			Assert(subkey != (ScanKey) DatumGetPointer(skey->sk_argument));
 			subkey--;
-			if ((subkey->sk_flags & SK_BT_REQFWD) &&
-				ScanDirectionIsForward(dir))
+			if (forcenonrequired)
+			{
+				/* treating scan's keys as non-required */
+			}
+			else if ((subkey->sk_flags & SK_BT_REQFWD) &&
+					 ScanDirectionIsForward(dir))
 				*continuescan = false;
 			else if ((subkey->sk_flags & SK_BT_REQBKWD) &&
 					 ScanDirectionIsBackward(dir))
@@ -2811,7 +3134,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
 			break;
 	}
 
-	if (!result)
+	if (!result && !forcenonrequired)
 	{
 		/*
 		 * Tuple fails this qual.  If it's a required qual for the current
@@ -2855,6 +3178,8 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate,
 	OffsetNumber aheadoffnum;
 	IndexTuple	ahead;
 
+	Assert(!pstate->forcenonrequired);
+
 	/* Avoid looking ahead when comparing the page high key */
 	if (pstate->offnum < pstate->minoff)
 		return;
-- 
2.49.0

#88Aleksander Alekseev
aleksander@timescale.com
In reply to: Peter Geoghegan (#87)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Hi Peter,

I'm now very close to committing everything. Though I do still want
another pair of eyes on the newer
0003-Improve-skip-scan-primitive-scan-scheduling.patch stuff before
commiting (since I still intend to commit all the remaining patches
together).

Can you think of any tests specifically for 0003, or relying on the
added Asserts() is best we can do? Same question for 0002.

I can confirm that v33 applies and passes the test.

0002 adds _bt_set_startikey() to nbtutils.c but it is not well-covered
by tests, many branches of the new code are never executed.

```
@@ -2554,9 +2865,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
              */
             if (requiredSameDir)
                 *continuescan = false;
+            else if (unlikely(key->sk_flags & SK_BT_SKIP))
+            {
+                /*
+                 * If we're treating scan keys as nonrequired, and encounter a
+                 * skip array scan key whose current element is NULL, then it
+                 * must be a non-range skip array
+                 */
+                Assert(forcenonrequired && *ikey > 0);
+                return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
+                                              tupdesc, *ikey, false);
+            }
```

This branch is also never executed during the test run.

In 0003:

```
@@ -2006,6 +2008,10 @@ _bt_advance_array_keys(IndexScanDesc scan,
BTReadPageState *pstate,
     else if (has_required_opposite_direction_only && pstate->finaltup &&
              unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup)))
     {
+        /*
+         * Make sure that any SAOP arrays that were not marked required by
+         * preprocessing are reset to their first element for this direction
+         */
         _bt_rewind_nonrequired_arrays(scan, dir);
         goto new_prim_scan;
     }
```

This branch is never executed too. This being said, technically there
is no new code here.

For your convenience I uploaded a complete HTML code coverage report
(~36 Mb) [1]https://drive.google.com/file/d/1breVpHapvJLtw8AlFAdXDAbK8ZZytY6v/view?usp=sharing.

[1]: https://drive.google.com/file/d/1breVpHapvJLtw8AlFAdXDAbK8ZZytY6v/view?usp=sharing

--
Best regards,
Aleksander Alekseev

In reply to: Aleksander Alekseev (#88)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Apr 1, 2025 at 9:26 AM Aleksander Alekseev
<aleksander@timescale.com> wrote:

Can you think of any tests specifically for 0003, or relying on the
added Asserts() is best we can do? Same question for 0002.

There are many more tests than those that are included in the patch. I
wrote and debugged all patches using TDD. This is also how I wrote the
Postgres 17 SAOP patch. (My test suite is actually a greatly expanded
version of the earlier SAOP patch's test suite, since this project is
a direct follow-up.)

These tests are not nearly stable enough to commit; they have
something like a 5% chance of spuriously failing when I run them,
generally due to concurrent autoanalyze activity that prevents VACUUM
from setting all VM bits. Almost all of the tests expect specific
index page accesses, which is captured by "Buffers:" output. I find
that the full test suite takes over 10 seconds to run with a debug
build. So they're really not too maintainable.

Every little behavioral detail has been memorialized by some test.
Many of the tests present some adversarial scenario where
_bt_advance_array_keys runs out of array keys before reaching the end
of a given page, where it must then decide how to get to the next leaf
page (the page whose key space matches the next distinct set of array
keys). We want to consistently make good decisions about whether we
should step to the next sibling page, or whether starting another
primscan to get to the next relevant leaf page makes more sense. That
comes up again and again (only a minority of these tests were written
to test general correctness).

I did write almost all of these tests before writing the code that
they test, but most of the tests never failed by giving wrong answers
to queries. They initially failed by not meeting some expectation that
I had in mind about how best to schedule primitive index scans.

What "the right decision" means when scheduling primitive index scans
is somewhat open to interpretation -- I do sometimes revise my opinion
in light of new information, or new requirements (SAOP scans have
basically the same issues as skip scans, but in practice skip scans
are more sensitive to these details). It's inherently necessary to
manage the uncertainty around which approach is best, in any given
situation, on any given page. Having a large and varied set of
scenarios seems to help to keep things in balance (it avoids unduly
favoring one type of scan or index over another).

I can confirm that v33 applies and passes the test.

0002 adds _bt_set_startikey() to nbtutils.c but it is not well-covered
by tests, many branches of the new code are never executed.

It's hard to get test coverage for these cases into the standard
regression tests, since the function is only called when on the second
or subsequent page of a given primscan -- you need quite a few
relatively big indexes. There are quite a few far-removed
implementation details that any test will inevitably have to rely on,
such as BLCKSZ, the influence of suffix truncation.

Just having test coverage of these branches wouldn't test much on its
own. In practice it's very likely that not testing the key directly
(incorrectly assuming that the _bt_keep_natts_fast precheck is enough,
just removing the uncovered branches) won't break queries at all. The
_bt_keep_natts_fast precheck tells us which columns have no more than
one distinct value on the page. If there's only one value, and if we
just left a page that definitely satisfied its keys, it's quite likely
that those same keys will also be satisfied on this new page (it's
just not certain, which is why _bt_set_startikey can't just rely on
the precheck, it has to test each key separately).

```
@@ -2554,9 +2865,20 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
*/
if (requiredSameDir)
*continuescan = false;
+            else if (unlikely(key->sk_flags & SK_BT_SKIP))
+            {
+                /*
+                 * If we're treating scan keys as nonrequired, and encounter a
+                 * skip array scan key whose current element is NULL, then it
+                 * must be a non-range skip array
+                 */
+                Assert(forcenonrequired && *ikey > 0);
+                return _bt_advance_array_keys(scan, NULL, tuple, tupnatts,
+                                              tupdesc, *ikey, false);
+            }
```

This branch is also never executed during the test run.

FWIW I have a test case that breaks when this particular code is
removed, given a wrong answer.

This is just another way that _bt_check_compare can find that it needs
to advance the array keys in the context of forcenonrequired mode --
it's just like the other calls to _bt_advance_array_keys from
_bt_check_compare. This particular code path is only hit when a skip
array's current element is NULL, which is unlikely to coincide with
the use of forcenonrequired mode (NULL is just another array element).
We just need another _bt_advance_array_keys callsite because of how
things are laid out in _bt_check_compare, which deals with
ISNULL-marked keys separately.

For your convenience I uploaded a complete HTML code coverage report
(~36 Mb) [1].

Thanks. I actually generated a similar coverage report myself
yesterday. I'm keeping an eye on this, but I don't think that it's
worth aiming for very high test coverage for these code paths. Writing
a failing test case for that one ISNULL _bt_advance_array_keys code
path was quite difficult, and required quite a big index. That isn't
going to be acceptable within the standard regression tests.

--
Peter Geoghegan

#90Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Peter Geoghegan (#86)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, 28 Mar 2025 at 22:59, Peter Geoghegan <pg@bowt.ie> wrote:

On Tue, Mar 25, 2025 at 7:45 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v31, which has a much-improved _bt_skip_ikeyprefix (which
I've once again renamed, this time to _bt_set_startikey).

Attached is v32

Thanks!

The review below was started for v31, then adapted to v32 when that
arrived. I haven't cross-checked this with v33.

Review for 0001:

I have some comments on the commit message, following below. _Note:
For smaller patches I would let small things like this go, but given
the complexity of the feature I think it is important to make sure
there can be no misunderstanding about how it works and why it's
correct. Hence, these comments._

Teach nbtree composite index scans to opportunistically skip over
irrelevant sections of composite indexes given a query with an omitted
prefix column.

AFAIK, this is only the second reference to "composite" indexes, after
a single mention in a comment in nbtsplitloc.c's _bt_strategy. IIUC,
this refers to multi-column indexes, which is more frequently and more
consistently used across the code base.

FYI, I think "composite index" would be more likely to refer to GIN,
given its double-btree structure. If we are about to use this term
more often, an entry in the glossary would be in place.

When nbtree is passed input scan keys derived from a
query predicate "WHERE b = 5", new nbtree preprocessing steps now output
"WHERE a = ANY(<every possible 'a' value>) AND b = 5" scan keys.

[...] new nbtree preprocessing steps now output +the equivalent of+ [...]

Preprocessing can freely add a skip array before or after any input
ScalarArrayOp arrays.

This implies skip arrays won't be generated for WHERE b = 5 (a
non-SAOP scankey) or WHERE b < 3 (not SAOP either), which is probably
not intentional, so a rewording would be appreciated.

// nbtpreprocesskeys.c

+static bool _bt_s**parray_shrink

I'd like to understand the "shrink" here, as it's not entirely clear to me.
The functions are exclusively called through dispatch in
_bt_compare_array_scankey_args, and I'd expected that naming to be
extended to these functions.

+ * This qual now becomes "WHERE x = ANY('{every possible x value}') and y = 4"

I understand what you're going for, but a reference that indexed NULLs
are still handled correctly (rather than removed by the filter) would
be appreciated, as the indicated substitution would remove NULL
values. (This doesn't have to be much more than a footnote.)

+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller must add this many
+ * skip arrays to each of the most significant attributes lacking any keys

I don't think that's a good description; as any value other than 0 or
1 would mean more than one skip array per such attribute, which is
obviously incorrect. I think I'd word it like:

+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller will have to add
+ * this many skip arrays: one for each of the most significant attributes
+ * lacking any keys that use the = strategy [...]
+#if 0
+                /* Could be a redundant input scan key, so can't do this: */
+                Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+                       (inkey->sk_flags & SK_SEARCHNULL));
+#endif

I think this should be removed?

+#ifdef DEBUG_DISABLE_SKIP_SCAN

I noticed this one and only reference to DEBUG_DISABLE_SKIP_SCAN. Are
you planning on keeping that around, or is this a leftover?

+        ScanKey        inkey = scan->keyData + i;
+
+        /*
+         * Backfill skip arrays for any wholly omitted attributes prior to
+         * attno_inkey
+         */
+        while (attno_skip < attno_inkey)

I don't understand why we're adding skip keys before looking at the
contents of this scankey, nor why we're backfilling skip keys based on
a now old value of attno_inkey. Please
add some comments on how/why this is the right approach.

prev_numSkipArrayKeys, *numSkipArrayKeys

I'm a bit confused why we primarily operate on *numSkipArrayKeys,
rather than a local variable that we store in *numSkipArrayKeys once
we know we can generate skip keys. I.e., I'd expected something more
in line with the following snippet (incomplete code blocks):

+            if (!OidIsValid(skip_eq_ops[attno_skip - 1]))
+            {
+                /*
+                 * Cannot generate a skip array for this or later attributes
+                 * (input opclass lacks an equality strategy operator)
+                 */
+                return numArrayKeys + *numSkipArrayKeys;
+            }
+
+            /* plan on adding a backfill skip array for this attribute */
+            loc_numSkipArrayKeys++;
+            attno_skip++;
+        }
+
+        *numSkipArrayKeys = loc_numSkipArrayKeys;

I think this is easier for the compiler to push the store operation
out of the loop (assuming it's optimizable at all; but even if it
isn't it removes the load of *numSkipArrayKeys from the hot path).

// utils/skipsupport.h, nbtutils.c

I think the increment/decrement callbacks for skipsupport should
explicitly check (by e.g. Assert) for NULL (or alternatively: same
value) returns on overflow, and the API definition should make this
explicit. The current system is quite easy to build a leaking
implementation for. Sure, there are other easy ways to break this, but
I think it's an easy API modification that makes things just that bit
safer.

A renewed review for 0002+ will arrive at a later point.

Kind regards,

Matthias van de Meent
Neon (https://neon.tech)

#91Alena Rybakina
a.rybakina@postgrespro.ru
In reply to: Matthias van de Meent (#90)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 01.04.2025 17:39, Matthias van de Meent wrote:

On Fri, 28 Mar 2025 at 22:59, Peter Geoghegan<pg@bowt.ie> wrote:

On Tue, Mar 25, 2025 at 7:45 PM Peter Geoghegan<pg@bowt.ie> wrote:

Attached is v31, which has a much-improved _bt_skip_ikeyprefix (which
I've once again renamed, this time to _bt_set_startikey).

Attached is v32
+static bool _bt_s**parray_shrink

I'd like to understand the "shrink" here, as it's not entirely clear to me.
The functions are exclusively called through dispatch in
_bt_compare_array_scankey_args, and I'd expected that naming to be
extended to these functions.

I understood _bt_skiparray_shrink() as the function that refines the
range of values that a skip array will consider,
by interpreting existing scalar inequality conditions and applying them
to limit the bounds of the skip scan.
I understood "shrink" to mean narrowing the range of values that the
skip array will consider during the index scan.

--
Regards,
Alena Rybakina
Postgres Professional

In reply to: Matthias van de Meent (#90)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Apr 1, 2025 at 10:40 AM Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:

The review below was started for v31, then adapted to v32 when that
arrived. I haven't cross-checked this with v33.

There's been hardly any changes to 0001- in quite a while, so that's fine.

Teach nbtree composite index scans to opportunistically skip over
irrelevant sections of composite indexes given a query with an omitted
prefix column.

AFAIK, this is only the second reference to "composite" indexes, after
a single mention in a comment in nbtsplitloc.c's _bt_strategy. IIUC,
this refers to multi-column indexes, which is more frequently and more
consistently used across the code base.

I don't think it makes much difference, but sure, I'll use the
multi-column index terminology. In both code comments, and in commit
messages.

When nbtree is passed input scan keys derived from a
query predicate "WHERE b = 5", new nbtree preprocessing steps now output
"WHERE a = ANY(<every possible 'a' value>) AND b = 5" scan keys.

[...] new nbtree preprocessing steps now output +the equivalent of+ [...]

Preprocessing can freely add a skip array before or after any input
ScalarArrayOp arrays.

This implies skip arrays won't be generated for WHERE b = 5 (a
non-SAOP scankey) or WHERE b < 3 (not SAOP either), which is probably
not intentional, so a rewording would be appreciated.

I don't agree. Yes, that sentence (taken in isolation) does not make
it 100% unambiguous. But, would anybody ever actually be misled? Even
once, ever? The misinterpretation of my words that you're concerned
about is directly contradicted by the whole opening paragraph. Plus,
it just doesn't make any sense. Obviously, you yourself never actually
interpreted it that way. Right?

The paragraph that this sentence appears in is all about the various
ways in which SAOP arrays and skip arrays are the same thing, except
at the very lowest level of the _bt_advance_array_keys code. I think
that that context makes it particularly unlikely that anybody would
ever think that I mean to imply something about the ways in which
non-array keys can be composed alongside skip arrays.

I'm pushing back here because I think that there's a real cost to
using overly defensive language, aimed at some imaginary person. The
problem with catering to such a person is that, overall, the
clarifications do more harm than good. What seems to end up happening
(I speak from experience with writing overly defensive comments) is
that the superfluous clarifications are read (by actual readers) the
wrong way -- they're read as if we must have meant quite a lot more
than what we actually intended.

More generally, I feel that it's a mistake to try to interpret your
words on behalf of the reader. While it's definitely useful to try to
anticipate the ways in which your reader might misunderstand, you
cannot reasonably do the work for them. It's usually (though not
always) best to deal with anticipated points of confusion by subtly
constructing examples that suggest that some plausible wrong
interpretation is in fact wrong, without drawing attention to it.
Coming right out and telling the reader what to not think *is* an
important tool, but it should be reserved for cases where it's truly
necessary.

// nbtpreprocesskeys.c

+static bool _bt_s**parray_shrink

I'd like to understand the "shrink" here, as it's not entirely clear to me.
The functions are exclusively called through dispatch in
_bt_compare_array_scankey_args, and I'd expected that naming to be
extended to these functions.

I don't see the problem? As Alena pointed out, we're shrinking the
arrays here (or are likely to), meaning that we're usually going to
eliminate some subset of array elements. It's possible that this will
happen more than once for a given array (since there could be more
than one "contradictory" key on input). An array can only shrink
within _bt_compare_array_scankey_args -- it can never have new array
elements added.

+ * This qual now becomes "WHERE x = ANY('{every possible x value}') and y = 4"

I understand what you're going for, but a reference that indexed NULLs
are still handled correctly (rather than removed by the filter) would
be appreciated, as the indicated substitution would remove NULL
values. (This doesn't have to be much more than a footnote.)

Why, though? Obviously, '{every possible x value}' isn't a valid array
literal. Doesn't that establish that this isn't a 100% literal
statement of fact?

There are a handful of places where I make a similar statement (the
commit message is another one, as is selfuncs.c). I do make this same
point about NULLs being just another value in selfuncs.c, though only
because it's relevant there. I don't want to talk about NULLs here,
because they just aren't relevant to this high-level overview at the
top of _bt_preprocess_keys. We do talk about the issue of skip arrays
and IS NOT NULL constraints elsewhere in nbtree: we talk about those
issues in _bt_first (shortly after _bt_preprocess_keys is called).

Again, it comes down to what the reader might actually be confused by,
in the real world. Is it really plausible that I could ever commit a
skip scan patch that wholly forgot to deal with NULLs sensibly? Would
you personally ever think that I could make such an obvious blunder in
a committed patch? And if you did, how long would it take you to
figure out that there was no such oversight?

+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller must add this many
+ * skip arrays to each of the most significant attributes lacking any keys

I don't think that's a good description; as any value other than 0 or
1 would mean more than one skip array per such attribute, which is
obviously incorrect. I think I'd word it like:

+ * Also sets *numSkipArrayKeys to # of skip arrays _bt_preprocess_array_keys
+ * caller must add to the scan keys it'll output.  Caller will have to add
+ * this many skip arrays: one for each of the most significant attributes
+ * lacking any keys that use the = strategy [...]

I agree that it's better your way. Will fix.

+#if 0
+                /* Could be a redundant input scan key, so can't do this: */
+                Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+                       (inkey->sk_flags & SK_SEARCHNULL));
+#endif

I think this should be removed?

Why? This is me expressing that I wish I could write this assertion,
but it won't quite work. I cannot rule out rare corner-cases involving
a contradictory pair of input keys, only one of which is a = key (the
other might be some kind of inequality, which makes this would-be
assertion not quite work). (You'll see similar "almost assertions"
from time to time, in different parts of the codebase.)

+#ifdef DEBUG_DISABLE_SKIP_SCAN

I noticed this one and only reference to DEBUG_DISABLE_SKIP_SCAN. Are
you planning on keeping that around, or is this a leftover?

I deliberately left this in place, just in case somebody wants to see
what happens when preprocessing stops generating skip arrays entirely.
Without this, it's not too obvious that it can just be turned off by
forcing _bt_num_array_keys to return early.

+        ScanKey        inkey = scan->keyData + i;
+
+        /*
+         * Backfill skip arrays for any wholly omitted attributes prior to
+         * attno_inkey
+         */
+        while (attno_skip < attno_inkey)

I don't understand why we're adding skip keys before looking at the
contents of this scankey, nor why we're backfilling skip keys based on
a now old value of attno_inkey. Please
add some comments on how/why this is the right approach.

_bt_num_array_keys should add exactly the minimum number of skip
arrays that will allow the standard _bt_preprocess_keys logic to mark
every input scan key (copied from scan->keyData[]) as required to
continue the scan on output (when output to so->keyData[]). It just
makes sense to structure the loop in a way that adds skip arrays just
before moving on to some input scan key on some never-before-seen
index column -- that's just how this needs to work.

Very early versions of the patch added skip arrays in cases involving
inequalities that could already be marked required. That approach
could probably work, but has no advantages, and some downsides. Now we
avoid adding skip arrays given a simple case like "WHERE a BETWEEN 1
AND 10". We only want to do it in cases like "WHERE a BETWEEN 1 AND 10
AND b = 42" -- since adding a skip array on "a" is strictly necessary
to be able to mark the = key on "b" as required. In the latter case,
the loop inside _bt_num_array_keys will add a skip array on "a" once
it gets past the final "a" input key (i.e. once it sees that the next
key is the = key on "b"). In the former case, there is no key on "b",
and so we don't add any skip arrays at all (which is the correct
behavior).

BTW, this _bt_num_array_keys code was primarily tested by writing lots
of complicated cases that tickled every edge-case I could think of. I
wrote tests that relied on my nbtree scan instrumentation patch, which
can print the details of both input and output keys -- I didn't rely
on testing any runtime behavior for this (that wouldn't have worked as
well). This includes any key markings (markings such as
SK_BT_REQFWD/SK_BT_REQBKWD), which is what I expected to see on every
scan key output (barring a couple of special cases, such as the
RowCompare case). Again, that is all that the "where do we add skip
arrays?" logic in _bt_num_array_keys is concerned with.

prev_numSkipArrayKeys, *numSkipArrayKeys

I'm a bit confused why we primarily operate on *numSkipArrayKeys,
rather than a local variable that we store in *numSkipArrayKeys once
we know we can generate skip keys. I.e., I'd expected something more
in line with the following snippet (incomplete code blocks):
I think this is easier for the compiler to push the store operation
out of the loop (assuming it's optimizable at all; but even if it
isn't it removes the load of *numSkipArrayKeys from the hot path).

What if I just had a local copy of numSkipArrayKeys, and copied back
into caller's arg when the function returns? We'll still need a
prev_numSkipArrayKeys under this scheme, but we won't have to set the
caller's pointer until right at the end anymore (which, I agree, seems
like it might be worth avoiding).

I think the increment/decrement callbacks for skipsupport should
explicitly check (by e.g. Assert) for NULL (or alternatively: same
value) returns on overflow, and the API definition should make this
explicit.

But the API definition *does* specifically address the opclass
author's responsibilities around NULLs? It specifically says that it's
not up to the opclass author to think about them at all.

The current system is quite easy to build a leaking
implementation for. Sure, there are other easy ways to break this, but
I think it's an easy API modification that makes things just that bit
safer.

How can I do that? The callbacks themselves (the callbacks for
functions that are called as the scan progresses) don't use the fmgr
interface.

They're simple C function pointers (rather like sort support
callbacks), and do not have any args related to NULLs. They accept a
raw Datum, which can never be a NULL. The convention followed by
similar functions that are passed a Datum that might just be a NULL is
for the function to also accept a separate "bool isnull" argument.
(Just not having such a separate bool arg is another existing
convention, and the one that I've followed here.)

A renewed review for 0002+ will arrive at a later point.

Thanks for the review!

--
Peter Geoghegan

#93Alena Rybakina
a.rybakina@postgrespro.ru
In reply to: Peter Geoghegan (#85)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Hi! Sorry for my later feedback, I didn't have enough time because of my
work and the conference that was held during these two days.

On 28.03.2025 23:15, Peter Geoghegan wrote:

On Thu, Mar 27, 2025 at 6:03 PM Alena Rybakina
<a.rybakina@postgrespro.ru> wrote:

I replied an example like this:

This example shows costs that are dominated by heap access costs. Both
the sequential scan and the bitmap heap scan must access 637 heap
blocks. So I don't think that this is such a great example -- the heap
accesses are irrelevant from the point of view of assessing how well
we're modelling index scan related costs.

You are right, the example was not very successful, to be honest I
couldn't find better example.

I think it would be useful to show information that we used an index scan but at the same time we skipped the "region" column and I assume we should output how many distinct values the "region" column had.

For example it will look like this "Skip Scan on region (4 distinct values)":
What do you think?

As I said on our call today, I think that we should keep the output
for EXPLAIN ANALYZE simple. While I'm sympathetic to the idea that we
should show more information about how quals can be applied in index
scan node output, that seems like it should be largely independent
work to me.

Masahiro Ikeda wrote a patch that aimed to improve matters in this
area some months back. I'm supportive of that (there is definitely
value in signalling to users that the index might actually look quite
different to how they imagine it looks, say by having an
omitted-by-query prefix attribute).

Thank you for your insights and explanation. I agree that this is a
separate piece of work, and I’ll definitely take a closer
look at Masahiro Ikeda’s contributions.

I don't exactly know what the most
useful kind of information to show is with skip scan in place, since
skip scan makes the general nature of quals (whether a given qual is
what oracle calls "access predicates", or what oracle calls "filter
predicates") is made squishy/dynamic by skip scan, in a way that is
new.

The relationship between the number of values that a skip array ever
uses, and the number of primitive index scans is quite complicated.
Sometimes it is actually as simple as your example query, but that's
often not true. "Index Searches: N" can be affected by:

* The use of SAOP arrays, which also influence primitive scan
scheduling, in the same way as they have since Postgres 17 -- and can
be mixed freely with skip arrays.

I see that this work is really voluminous and yes, I agree with you that
optimization for skipping index scanning
in terms of displaying information about changing quals, if any, can
even be done using Oracle as an example.
For example, if you introduce a new range ANY(<every possible 'a'
value>) due to skipping the first column
in the index key, it will be useful for users to know. Or if you define
a new range that the skip array will consider
during the index scan. Well, and other things related to this, but not
specifically with your patch, for example
in the case of conflicting conditions or defining boundaries in the case
of index scanning, like, when a>1 and a>10
we need to scan only a>10.

This optimization was also introduced by you earlier even before the
patch on skip optimization, but I think it also lacks
some output in the explain.

* The availability of opclass skipsupport, which makes skip arrays
generate their element values by addition/subtraction from the current
array element, rather than using NEXT/PRIOR sentinel keys.

The sentinel keys act as probes that get the next real (non-sentinel)
value that we need to look up next. Whereas skip support can often
successfully guess that (for example) the next value in the index
after 268 is 269, saving a primitive scan each time (this might not
happen at all, or it might work only some of the time, or it might
work all of the time).

To be honest, I missed your point here. If possible, could you explain
it in more detail?

* Various primitive index scan scheduling heuristics.

Another concern here is that I don't want to invent a special kind of
"index search" just for skip scan. We're going to show an "Index
Searches: N" that's greater than 1 with SAOP array keys, too -- which
don't use skip scan at all (nothing new about that, except for the
fact that we report the number of searches directly from EXPLAIN
ANALYZE in Postgres 18).

I agree with you, this is an improvement. "Index Searches: N" shows the
number of individual index searches, but
it is still not clear enough. Here you can additionally determine what
happened based on the information about
the number of scanned pages, but with large amounts of data this is
difficult.

By the way, are you planning to commit additional output to the explain
about skipped pages? I think, together with
the previous ones, the information about index scanning would be
sufficient for analysis.

Although I am still learning to understand correctly this information in
the explain.
By the way, I have long wanted to ask, maybe you can advise something
else to read on this topic?
Maybe not in this thread, so as not to overload this.

There really is almost no difference between
a scan with a skip array and a scan of the same index with a similar
SAOP array (when each array "contains the same elements", and is used
to scan the same index, in the same way). That's why the cost model is
as similar as possible to the Postgres 17 costing of SAOP array scans
-- it's really the same access method. Reusing the cost model makes
sense because actual execution times are almost identical when we
compare a skip array to a SAOP array in the way that I described.

Yes, I agree with that.

The only advantage that I see from putting something about "skip scan"
in EXPLAIN ANALYZE is that it is more googleable that way. But it
seems like "Index Searches: N" is almost as good, most of the time. In
any case, the fact that we don't need a separate optimizer index
path/executor node for this is something that I see as a key
advantage, and something that I'd like EXPLAIN ANALYZE to preserve.

I think it is worth adding "skip scan" information, without it it is
difficult in my opinion to evaluate
whether this index is effective in comparison with another, looking only
at the information on
scanned blocks or Index search or I missed something?

I'm not sure that I correctly understood about "separation optimizer
index path/executor stage". Do you mean that it's better to do all the
optimizations during index execution rather than during index execution?
I just remember you mentioned the Goetz Graefe interview on the call and
and this was precisely his point of view, with which you agree. Is that
what you mean?

The problem with advertising that an index scan node is a skip scan
is: what if it just never skips? Never skipping like this isn't
necessarily unexpected. And even if it is unexpected, it's not
necessarily a problem.

I agree that there may not be a place for this to be used, but it is
worth showing information about it if it does happen.

On the other hand, here we need to be able to determine when it was
necessary to perform skip scan optimization, but
it was not there. But I'm not sure that it is possible to display this
in the explain - only when analyzing the received query plan,
namely the buffer statistics.

I didn't see any regression tests. Maybe we should add some tests? To be honest I didn't see it mentioned in the commit message but I might have missed something.

There are definitely new regression tests -- I specifically tried to
keep the test coverage high, using gcov html reports (like the ones
from coverage.postgresql.org). The test updates appear towards the end
of the big patch file, though. Maybe you're just not used to seeing
tests appear last like this?

I use "git config diff.orderfile ... " to get this behavior. I find it
useful to put the important changes (particularly header file changes)
first, and less important changes (like tests) much later.

Thanks for taking a look at my patch!

Yes, indeed, everything is in place, sorry, I didn't notice it right
away, I'll be more attentive and I'll take note of your advice about the
git - I haven't used such methods before)

--
Regards,
Alena Rybakina
Postgres Professional

#94Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Peter Geoghegan (#92)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, 1 Apr 2025 at 21:02, Peter Geoghegan <pg@bowt.ie> wrote:

On Tue, Apr 1, 2025 at 10:40 AM Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:

When nbtree is passed input scan keys derived from a
query predicate "WHERE b = 5", new nbtree preprocessing steps now output
"WHERE a = ANY(<every possible 'a' value>) AND b = 5" scan keys.

[...] new nbtree preprocessing steps now output +the equivalent of+ [...]

Preprocessing can freely add a skip array before or after any input
ScalarArrayOp arrays.

This implies skip arrays won't be generated for WHERE b = 5 (a
non-SAOP scankey) or WHERE b < 3 (not SAOP either), which is probably
not intentional, so a rewording would be appreciated.

I don't agree. Yes, that sentence (taken in isolation) does not make
it 100% unambiguous. But, would anybody ever actually be misled? Even
once, ever? The misinterpretation of my words that you're concerned
about is directly contradicted by the whole opening paragraph. Plus,
it just doesn't make any sense. Obviously, you yourself never actually
interpreted it that way. Right?

That's built on my knowledge about the internals of this patch ahead
of reading the message, not on a clean-sleet interpretation.

The paragraph that this sentence appears in is all about the various
ways in which SAOP arrays and skip arrays are the same thing, except
at the very lowest level of the _bt_advance_array_keys code. I think
that that context makes it particularly unlikely that anybody would
ever think that I mean to imply something about the ways in which
non-array keys can be composed alongside skip arrays.

I'm pushing back here because I think that there's a real cost to
using overly defensive language, aimed at some imaginary person.

While I agree that there is such a cost, I don't think that this is
too far fetched. They are not just added when we have SAOP scankeys,
and I think the non-SAOP case is one of the most important areas where
this patch improves performance. Yes, this patch improves performance
for queries with only SAOP on non-first keys, but I've seen more
non-SAOP queries where this patch would improve performance than
similar queries but with SAOP.

// nbtpreprocesskeys.c

+static bool _bt_s**parray_shrink

I'd like to understand the "shrink" here, as it's not entirely clear to me.
The functions are exclusively called through dispatch in
_bt_compare_array_scankey_args, and I'd expected that naming to be
extended to these functions.

I don't see the problem?

It's mostly as an observation (and not problem per se) that "compare"
(which sounds like a pure function that doens't modify anything, e.g.
_bt_compare) is used to dispatch to "shrink" (which does sound like
it'd modify something).

+ * This qual now becomes "WHERE x = ANY('{every possible x value}') and y = 4"

I understand what you're going for, but a reference that indexed NULLs
are still handled correctly (rather than removed by the filter) would
be appreciated, as the indicated substitution would remove NULL
values. (This doesn't have to be much more than a footnote.)

Again, it comes down to what the reader might actually be confused by,
in the real world. Is it really plausible that I could ever commit a
skip scan patch that wholly forgot to deal with NULLs sensibly? Would
you personally ever think that I could make such an obvious blunder in
a committed patch? And if you did, how long would it take you to
figure out that there was no such oversight?

My comment is not about your potential future actions, but rather what
any future developer or committer working on this code might think and
worry about when reading this. = ANY {} constructs *always* have NOT
NULL behaviour, just like any other operator clause that isn't "IS
NULL", so clarifying that this is only similar -and does not behave
the same in important edge cases- is IMO important. Not specifically
for you, but for any other developer trying to get a correct
understanding of how this works and why it is correct.

+#if 0
+                /* Could be a redundant input scan key, so can't do this: */
+                Assert(inkey->sk_strategy == BTEqualStrategyNumber ||
+                       (inkey->sk_flags & SK_SEARCHNULL));
+#endif

I think this should be removed?

Why? This is me expressing that I wish I could write this assertion,
but it won't quite work. I cannot rule out rare corner-cases involving
a contradictory pair of input keys, only one of which is a = key (the
other might be some kind of inequality, which makes this would-be
assertion not quite work). (You'll see similar "almost assertions"
from time to time, in different parts of the codebase.)

It is my understanding that those are mostly historical artifacts of
the code base, and not systems in active development. Their rarety
makes it difficult to put numbers on, but IIRC at least one such case
was recently removed for bitrot and apparent lack of use in years.

+#ifdef DEBUG_DISABLE_SKIP_SCAN

I noticed this one and only reference to DEBUG_DISABLE_SKIP_SCAN. Are
you planning on keeping that around, or is this a leftover?

I deliberately left this in place, just in case somebody wants to see
what happens when preprocessing stops generating skip arrays entirely.
Without this, it's not too obvious that it can just be turned off by
forcing _bt_num_array_keys to return early.

Ah, I see.

prev_numSkipArrayKeys, *numSkipArrayKeys

I'm a bit confused why we primarily operate on *numSkipArrayKeys,
rather than a local variable that we store in *numSkipArrayKeys once
we know we can generate skip keys. I.e., I'd expected something more
in line with the following snippet (incomplete code blocks):
I think this is easier for the compiler to push the store operation
out of the loop (assuming it's optimizable at all; but even if it
isn't it removes the load of *numSkipArrayKeys from the hot path).

What if I just had a local copy of numSkipArrayKeys, and copied back
into caller's arg when the function returns? We'll still need a
prev_numSkipArrayKeys under this scheme, but we won't have to set the
caller's pointer until right at the end anymore (which, I agree, seems
like it might be worth avoiding).

That's a nice alternative too, indeed.

I think the increment/decrement callbacks for skipsupport should
explicitly check (by e.g. Assert) for NULL (or alternatively: same
value) returns on overflow, and the API definition should make this
explicit.

But the API definition *does* specifically address the opclass
author's responsibilities around NULLs? It specifically says that it's
not up to the opclass author to think about them at all.

Yes. What I'm suggesting is to make the contract enforceable to a
degree. If it was defined to "return (Datum) 0 on overflow", then that
could be Assert()ed; and code that does leak memory could get detected
by static analysis tools in the function scope because the allocation
didn't get returned, but with this definition returning an allocation
is never detected because that's not part of the contract.

The current system is quite easy to build a leaking
implementation for. Sure, there are other easy ways to break this, but
I think it's an easy API modification that makes things just that bit
safer.

How can I do that? The callbacks themselves (the callbacks for
functions that are called as the scan progresses) don't use the fmgr
interface.

You could Assert(inc_sk_argument == (Datum) 0) in the oflow branch?
I'm certain that (Datum) 0 is an invalid representation of a pointer,
so we know that no allocated value is returned (be it new or
pre-existing).

Kind regards,

Matthias van de Meent
Neon (https://neon.tech)

#95Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Peter Geoghegan (#87)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, 1 Apr 2025 at 04:02, Peter Geoghegan <pg@bowt.ie> wrote:

On Fri, Mar 28, 2025 at 5:59 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v32, which has very few changes, but does add a new patch:
a patch that adds skip-array-specific primitive index scan heuristics
to _bt_advance_array_keys (this is
v32-0003-Improve-skip-scan-primitive-scan-scheduling.patch).

Attached is v33

0001:

I just realised we never check whether skip keys' high/low_compare
values generate valid quals, like what you'd see generated for WHERE a

5 AND a < 3 AND b = 2;

This causes a performance regression in the patched version:

-> Index Only Scan using test_a_b_idx on test (cost=0.14..8.16
rows=1 width=0) (actual time=0.240..0.241 rows=0.00 loops=1)
Index Cond: ((a > 5) AND (a < 3) AND (b = 2))
Heap Fetches: 0
Index Searches: 1
Buffers: shared hit=1

As you can see in this explain, we're doing an index search, while the
index searches attribute before this patch would've been 0 due to
conflicting conditions.

(This came up while reviewing 0004, when I thought about doing this
key consistency check after the increment/decrement optimization of
that patch and after looking couldn't find the skipscan bounds
consistency check at all)

0002:

// nbtutils.c

+ * (safe even when "key->sk_attno <= firstchangingattnum")

Typo: should be "key->sk_attno >= firstchangingattnum".

I'd also refactor the final segment to something like the following,
to remove a redundant compare when the attribute we're checking is
equal between firsttup and lasttup:

+        firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc,
&firstnull);
+
+        /* Test firsttup */
+        _bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+                                   firstdatum, firstnull, array, key,
+                                   &result);
+        if (result != 0)
+            break;                /* unsafe */
+
+        /* both attributes are equal, so no need to check lasttup */
+        if (key->sk_attno < firstchangingattnum)
+            continue;
+
+        lastdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &lastnull);
+
+        /* Test lasttup */
+        _bt_binsrch_skiparray_skey(false, ForwardScanDirection,
+                                   lastdatum, lastnull, array, key,
+                                   &result);
+        if (result != 0)
+            break;                /* unsafe */
+
+        /* Safe, range skip array satisfied by every tuple */

0003: LGTM

0004: LGTM, but note the current bug in 0001, which is probably best
solved with a fix that keeps this optimization in mind, too.

Kind regards,

Matthias van de Meent
Neon (https://neon.tech)

#96Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Matthias van de Meent (#95)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, 1 Apr 2025 at 23:56, Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:

On Tue, 1 Apr 2025 at 04:02, Peter Geoghegan <pg@bowt.ie> wrote:

On Fri, Mar 28, 2025 at 5:59 PM Peter Geoghegan <pg@bowt.ie> wrote:

Attached is v32, which has very few changes, but does add a new patch:
a patch that adds skip-array-specific primitive index scan heuristics
to _bt_advance_array_keys (this is
v32-0003-Improve-skip-scan-primitive-scan-scheduling.patch).

Attached is v33

0001:

I just realised we never check whether skip keys' high/low_compare
values generate valid quals, like what you'd see generated for WHERE a

5 AND a < 3 AND b = 2;

This causes a performance regression in the patched version:

Apparently it's not a regression, as we don't have this check in place
in the master branch. So, that's an optimization for PG19.

Sorry for the noise.

Kind regards,

Matthias van de Meent
Neon (https://neon.tech)

In reply to: Matthias van de Meent (#95)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Apr 1, 2025 at 5:56 PM Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:

0002:

// nbtutils.c

+ * (safe even when "key->sk_attno <= firstchangingattnum")

Typo: should be "key->sk_attno >= firstchangingattnum".

Good catch!

Though I think it should be "" safe even when "key->sk_attno >
firstchangingattnum" "", to highlight that the rule here is
significantly more permissive than even the nearby range skip array
case, which is still safe when (key->sk_attno == firstchangingattnum).

As I'm sure you realize, SAOP = keys and regular = keys are only safe
when "key->sk_attno < firstchangingattnum". So there are a total of 3
distinct rules about how firstchangingattnum affects whether it's safe
to advance pstate.startikey past a scan key (which of the 3 rules we
apply depends solely on the type of scan key).

In summary, simple = keys have the strictest firstchangingattnum rule,
range skip arrays/scalar inequalities have a somewhat less restrictive
rule, and non-range skip arrays have the least restrictive/most
permissive rule. As I think you understand already, it is generally
safe to set pstate.startikey to an offset that's past several earlier
simple skip arrays (against several prefix columns, all omitted from
the query) -- even when firstchangingattnum is the lowest possible
value (which is attnum 1).

I'd also refactor the final segment to something like the following,
to remove a redundant compare when the attribute we're checking is
equal between firsttup and lasttup:

At one point the code did look like that, but I concluded that the
extra code wasn't really worth it. We can only save cycles within
_bt_set_startikey itself this way, which doesn't add up to much.
_bt_set_startikey is only called once per page.

Besides, in general it's fairly likely that a range skip array that
_bt_set_startikey sees won't be against a column that we already know
(from the _bt_keep_natts_fast precheck, which returns
firstchangingattnum) to only have one distinct value among all tuples
on the page.

0003: LGTM

0004: LGTM

Great, thanks!

--
Peter Geoghegan

In reply to: Matthias van de Meent (#94)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Apr 1, 2025 at 4:16 PM Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:

While I agree that there is such a cost, I don't think that this is
too far fetched. They are not just added when we have SAOP scankeys,
and I think the non-SAOP case is one of the most important areas where
this patch improves performance. Yes, this patch improves performance
for queries with only SAOP on non-first keys, but I've seen more
non-SAOP queries where this patch would improve performance than
similar queries but with SAOP.

That's all likely to be true. I just don't think that the commit
message for the big commit (the part that you took issue with) said
anything that suggests otherwise.

To recap, the sentence in question says "Preprocessing can freely add
a skip array before or after any input ScalarArrayOp arrays". This is
mostly just making a high level point about the design itself -- so I
just don't get what you mean.

The "everything is an array" design is what allows skip arrays to work
with a qual like "WHERE b IN (1, 2, 3)", as you say. It's also what
allows things like "WHERE a IN (100, 500) AND c = 55" to work
efficiently, without introducing any special cases -- it works both
ways. And, a pair of skip arrays can also be composed together, in
just the same way as a pair of SAOP arrays. This all works in the same
way; _bt_advance_array_keys concepts like array roll over continue to
apply, with essentially zero changes to the high level design,
relative to Postgres 17. That's the core idea that the paragraph in
question conveys.

Recall that _bt_advance_array_keys likes to think of simple scalar =
keys as a degenerate single-value array. They are the same thing, for
the purposes of rolling over the scan's arrays. We need to use a 3-way
ORDER proc for scalar scan keys for this reason.

It's mostly as an observation (and not problem per se) that "compare"
(which sounds like a pure function that doens't modify anything, e.g.
_bt_compare) is used to dispatch to "shrink" (which does sound like
it'd modify something).

It sounds like it's modifying something because (as you must know) it
does just that. This has been the case since the Postgres 17 SAOP
patch, of course (only the use of the term "shrink" in a helper
function is new here).

I don't want to rename _bt_compare_scankey_args now (that name is well
over 20 years old). That would be what it would take to make this
consistent in the way you expect. I just don't think it matters very
much.

My comment is not about your potential future actions, but rather what
any future developer or committer working on this code might think and
worry about when reading this. = ANY {} constructs *always* have NOT
NULL behaviour, just like any other operator clause that isn't "IS
NULL", so clarifying that this is only similar -and does not behave
the same in important edge cases- is IMO important.

Not specifically for you, but for any other developer trying to get a correct
understanding of how this works and why it is correct.

How many times does it have to be clarified, though? Do I have to put
something about it anywhere I give a brief high-level description of
how skip arrays work, where it's natural to compare them to a
traditional SAOP that generates all possible matching elements?
Explaining the concepts in question is hard enough, without having to
always list all of the ways that my analogy isn't the full and literal
truth of the matter. It's already extremely obvious that it must be
far from a literal account of what happens.

It is my understanding that those are mostly historical artifacts of
the code base, and not systems in active development. Their rarety
makes it difficult to put numbers on, but IIRC at least one such case
was recently removed for bitrot and apparent lack of use in years.

It's effectively a comment (nobody is expected to ever uncomment it by
removing the "#ifdef 0"). Sometimes, comments become obsolete. It's a
trade-off.

What if I just had a local copy of numSkipArrayKeys, and copied back
into caller's arg when the function returns? We'll still need a
prev_numSkipArrayKeys under this scheme, but we won't have to set the
caller's pointer until right at the end anymore (which, I agree, seems
like it might be worth avoiding).

That's a nice alternative too, indeed.

I'll do it that way in the commited patch. That's probably not going
to happen until Friday morning EST, to give me another full day to
work some more on the docs.

I don't see much point in posting another version of the patchset to the list.

But the API definition *does* specifically address the opclass
author's responsibilities around NULLs? It specifically says that it's
not up to the opclass author to think about them at all.

Yes. What I'm suggesting is to make the contract enforceable to a
degree. If it was defined to "return (Datum) 0 on overflow", then that
could be Assert()ed; and code that does leak memory could get detected
by static analysis tools in the function scope because the allocation
didn't get returned, but with this definition returning an allocation
is never detected because that's not part of the contract.

All B-Tree support functions aren't allowed to leak memory. Same with
all operators. This will be far from the only time that we expect
opclass authors to get that right. This mostly works just fine,
probably because an opclass that leaked memory like this would visibly
break quite quickly.

You could Assert(inc_sk_argument == (Datum) 0) in the oflow branch?
I'm certain that (Datum) 0 is an invalid representation of a pointer,
so we know that no allocated value is returned (be it new or
pre-existing).

I just don't see what the point would be. Nothing would stop a
decrement/increment callback that needs to allocate memory from
returning 0 and then leaking memory anyway.

--
Peter Geoghegan

In reply to: Alena Rybakina (#93)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Apr 1, 2025 at 3:08 PM Alena Rybakina <a.rybakina@postgrespro.ru> wrote:

I think it would be useful to show information that we used an index scan but at the same time we skipped the "region" column and I assume we should output how many distinct values the "region" column had.

For example it will look like this "Skip Scan on region (4 distinct values)":

What do you think?

I don't see much value in that. We can sometimes have data skew that
makes the number of distinct values far from representative of how
many index searches were required. We can have 3 distinct prefix
column values within 90% of all leaf pages, while the remaining 10%
all have unique values. Skip scan will work quite well here (at least
compared to a traditional full index scan), but the number of distinct
values makes it look really bad.

I see that this work is really voluminous and yes, I agree with you that optimization for skipping index scanning
in terms of displaying information about changing quals, if any, can even be done using Oracle as an example.
For example, if you introduce a new range ANY(<every possible 'a' value>) due to skipping the first column
in the index key, it will be useful for users to know. Or if you define a new range that the skip array will consider
during the index scan. Well, and other things related to this, but not specifically with your patch, for example
in the case of conflicting conditions or defining boundaries in the case of index scanning, like, when a>1 and a>10
we need to scan only a>10.

This optimization was also introduced by you earlier even before the patch on skip optimization, but I think it also lacks
some output in the explain.

I would also like this kind of stuff to appear in EXPLAIN. It's partly
hard because so much stuff happens outside of planning.

* The availability of opclass skipsupport, which makes skip arrays
generate their element values by addition/subtraction from the current
array element, rather than using NEXT/PRIOR sentinel keys.

The sentinel keys act as probes that get the next real (non-sentinel)
value that we need to look up next. Whereas skip support can often
successfully guess that (for example) the next value in the index
after 268 is 269, saving a primitive scan each time (this might not
happen at all, or it might work only some of the time, or it might
work all of the time).

To be honest, I missed your point here. If possible, could you explain it in more detail?

So, many types don't (and probably can't) offer skip support. Skip
scan still works there. The most common example of this is "text".

We're still using skip arrays when skipping using (say) a text column.
Conceptually, it's still "WHERE a = ANY(<every possible 'a' value>)",
even with these continuous data types. It is useful to "pretend that
we're using a discrete data type", so that everything can work in the
usual way (remember, I hate special cases). We need to invent another
way to "increment" a text datum, that works in the same way (but
doesn't really require understanding the semantics of text, or
whatever the data type may be).

See my explanation about this here:

/messages/by-id/CAH2-WznKyHq_W7heu87z80EHyZepQeWbGuAZebcxZHvOXWCU-w@mail.gmail.com

See the part of my email that begins with "I think that you're
probably still a bit confused because the terminology in this area is
a little confusing. There are two ways of explaining the situation
with types like text and numeric (types that lack skip support)...."

I agree with you, this is an improvement. "Index Searches: N" shows the number of individual index searches, but
it is still not clear enough. Here you can additionally determine what happened based on the information about
the number of scanned pages, but with large amounts of data this is difficult.

The benefit of using skip scan comes from all of the pages that we
*aren't* reading, that we'd usually have to read. Hard to show that.

By the way, are you planning to commit additional output to the explain about skipped pages? I think, together with
the previous ones, the information about index scanning would be sufficient for analysis.

Not for Postgres 18.

Although I am still learning to understand correctly this information in the explain.
By the way, I have long wanted to ask, maybe you can advise something else to read on this topic?
Maybe not in this thread, so as not to overload this.

Let's talk about it off list.

I think it is worth adding "skip scan" information, without it it is difficult in my opinion to evaluate
whether this index is effective in comparison with another, looking only at the information on
scanned blocks or Index search or I missed something?

I think that it's particularly worth adding something to EXPLAIN
ANALYZE that makes it obvious that the index in question might not be
what the user thinks that it is. It might be an index that does some
skipping, but is far from optimal. They might have simply overlooked
the fact that there is an "extra" column between the columns that
their query predicate actually uses, which is far slower than what is
possible with a separate index that also omits that column.

Basically, I think that the most important goal should be to try to
help the user to understand when they have completely the wrong idea
about the index. I think that it's much less important to help users
to understand exactly how well a skip scan performs, relative to some
theoretical ideal. The theoretical ideal is just too complicated.

I'm not sure that I correctly understood about "separation optimizer index path/executor stage". Do you mean that it's better to do all the optimizations during index execution rather than during index execution?

Yes. In general it's good if we can delay decisions about the scan's
behavior until runtime, when we have a more complete picture of what
the index actually looks like.

--
Peter Geoghegan

#100Alena Rybakina
a.rybakina@postgrespro.ru
In reply to: Peter Geoghegan (#99)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 03.04.2025 02:32, Peter Geoghegan wrote:

On Tue, Apr 1, 2025 at 3:08 PM Alena Rybakina<a.rybakina@postgrespro.ru> wrote:

I think it would be useful to show information that we used an index scan but at the same time we skipped the "region" column and I assume we should output how many distinct values the "region" column had.

For example it will look like this "Skip Scan on region (4 distinct values)":

What do you think?

I don't see much value in that. We can sometimes have data skew that
makes the number of distinct values far from representative of how
many index searches were required. We can have 3 distinct prefix
column values within 90% of all leaf pages, while the remaining 10%
all have unique values. Skip scan will work quite well here (at least
compared to a traditional full index scan), but the number of distinct
values makes it look really bad.

Yes, I agree — this could give a misleading impression when trying to
evaluate the effectiveness of the feature.
I’ve spent quite some time thinking about whether there’s a better way
to present this information, but I haven’t come up with a solid alternative.

To be honest, I’m starting to think that simply displaying the name of
the skipped column might be sufficient.

* The availability of opclass skipsupport, which makes skip arrays
generate their element values by addition/subtraction from the current
array element, rather than using NEXT/PRIOR sentinel keys.

The sentinel keys act as probes that get the next real (non-sentinel)
value that we need to look up next. Whereas skip support can often
successfully guess that (for example) the next value in the index
after 268 is 269, saving a primitive scan each time (this might not
happen at all, or it might work only some of the time, or it might
work all of the time).

To be honest, I missed your point here. If possible, could you explain it in more detail?

So, many types don't (and probably can't) offer skip support. Skip
scan still works there. The most common example of this is "text".

We're still using skip arrays when skipping using (say) a text column.
Conceptually, it's still "WHERE a = ANY(<every possible 'a' value>)",
even with these continuous data types. It is useful to "pretend that
we're using a discrete data type", so that everything can work in the
usual way (remember, I hate special cases). We need to invent another
way to "increment" a text datum, that works in the same way (but
doesn't really require understanding the semantics of text, or
whatever the data type may be).

See my explanation about this here:

/messages/by-id/CAH2-WznKyHq_W7heu87z80EHyZepQeWbGuAZebcxZHvOXWCU-w@mail.gmail.com

See the part of my email that begins with "I think that you're
probably still a bit confused because the terminology in this area is
a little confusing. There are two ways of explaining the situation
with types like text and numeric (types that lack skip support)...."

I understand it. I agree with you that it should be extended to other
types but I'm not sure how.

Maybe we can add an abstract iterator that will helps to get the next
distinct value adapted to the type or
it needs to be added similar functions for each type. I think this topic
is also for a separate thread)

I agree with you, this is an improvement. "Index Searches: N" shows the number of individual index searches, but
it is still not clear enough. Here you can additionally determine what happened based on the information about
the number of scanned pages, but with large amounts of data this is difficult.

The benefit of using skip scan comes from all of the pages that we
*aren't* reading, that we'd usually have to read. Hard to show that.

I noticed statistics on the number of hit buffers, read buffers, for
example, "Buffers: shared hit=3 read=52",
are you talking about this?

Although I am still learning to understand correctly this information in the explain.
By the way, I have long wanted to ask, maybe you can advise something else to read on this topic?
Maybe not in this thread, so as not to overload this.

Let's talk about it off list.

Okay. Thank you)

I think it is worth adding "skip scan" information, without it it is difficult in my opinion to evaluate
whether this index is effective in comparison with another, looking only at the information on
scanned blocks or Index search or I missed something?

I think that it's particularly worth adding something to EXPLAIN
ANALYZE that makes it obvious that the index in question might not be
what the user thinks that it is. It might be an index that does some
skipping, but is far from optimal. They might have simply overlooked
the fact that there is an "extra" column between the columns that
their query predicate actually uses, which is far slower than what is
possible with a separate index that also omits that column.

Basically, I think that the most important goal should be to try to
help the user to understand when they have completely the wrong idea
about the index. I think that it's much less important to help users
to understand exactly how well a skip scan performs, relative to some
theoretical ideal. The theoretical ideal is just too complicated.

Yes, I agree — this information can be valuable, especially for those
investigating query performance issues.
In particular, for example, it helps in cases where the optimizer
chooses a suboptimal index, and it's not obvious why.

I believe it’s important to display not just what expressions were used
during planning, but also what was actually used during execution.
That information might be important when analyzing data skew, deciding
whether extended statistics are needed, and
understanding how the planner's assumptions played out.

But this is a separate thread for discussion.

--
Regards,
Alena Rybakina
Postgres Professional

In reply to: Peter Geoghegan (#97)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Apr 1, 2025 at 7:02 PM Peter Geoghegan <pg@bowt.ie> wrote:

Though I think it should be "" safe even when "key->sk_attno >
firstchangingattnum" "", to highlight that the rule here is
significantly more permissive than even the nearby range skip array
case, which is still safe when (key->sk_attno == firstchangingattnum).

Mark Dilger reported a bug in commit 8a510275 on Saturday, which I
fixed in commit b75fedca from Monday. Mark's repro was a little bit
complicated, though.

Attached is a Python script that performs fuzz testing of nbtree skips
scan. It is capable of quickly finding the same bug as the one that
Mark reported. The script generates random, complicated multi-column
index scans on a (a, b, c, d) index on a test table, and verifies that
each queries gives the same answer as an equivalent sequential scan
plan. This works quite well as a general smoke test. I find that if I
deliberately add somewhat plausible bugs to the code in
_bt_set_startikey, the fuzz testing script is usually able to identify
wrong answers to queries in under a minute.

I don't expect that this script will actually discover any real bugs
-- I ran it for long enough to get the sense that that was unlikely.
But it seemed like a worthwhile exercise.

--
Peter Geoghegan

Attachments:

fuzz_skip_scan.pytext/x-python-script; charset=UTF-8; name=fuzz_skip_scan.pyDownload
#102Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Peter Geoghegan (#68)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Peter,

moving the conversation here from "pgsql: Improve nbtree skip scan
primitive scan scheduling" thread on -committers. Attached is another
regression test for your consideration, which gives rise to the following
assertion:

TRAP: failed Assert("numSkipArrayKeys == 0"), File: "nbtpreprocesskeys.c",
Line: 1859, PID: 7210
0 postgres 0x00000001032f30e0
ExceptionalCondition + 108
1 postgres 0x0000000102e83100
_bt_preprocess_keys + 6036
2 postgres 0x0000000102e87338 _bt_first + 168
3 postgres 0x0000000102e84680 btgettuple + 196
4 postgres 0x0000000102e75cdc
index_getnext_tid + 68
5 postgres 0x00000001030017a0 IndexOnlyNext +
228
6 postgres 0x0000000102fe5b2c ExecScan + 228
7 postgres 0x0000000103011088 ExecSort + 536
8 postgres 0x0000000102fdbc68
standard_ExecutorRun + 312
9 postgres 0x00000001031bdfb8 PortalRunSelect
+ 236
10 postgres 0x00000001031bdbd4 PortalRun + 492
11 postgres 0x00000001031bcb18
exec_simple_query + 1292
12 postgres 0x00000001031b9d1c PostgresMain +
1388
13 postgres 0x00000001031b59a8
BackendInitialize + 0
14 postgres 0x000000010310edd8
postmaster_child_launch + 372
15 postgres 0x000000010311303c ServerLoop + 4948
16 postgres 0x0000000103111360
InitProcessGlobals + 0
17 postgres 0x0000000103030c00 help + 0
18 dyld 0x000000018eb17154 start + 2476

This looks sufficiently different from the assertion mentioned on the other
thread to be worth posting here.


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

_skipscan2.sqlapplication/sql; name=_skipscan2.sqlDownload
In reply to: Mark Dilger (#102)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Apr 30, 2025 at 9:12 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:

TRAP: failed Assert("numSkipArrayKeys == 0"), File: "nbtpreprocesskeys.c", Line: 1859, PID: 7210
0 postgres 0x00000001032f30e0 ExceptionalCondition + 108
1 postgres 0x0000000102e83100 _bt_preprocess_keys + 6036

This looks sufficiently different from the assertion mentioned on the other thread to be worth posting here.

It is a completely unrelated issue. Fortunately, this one is harmless.
The assertion merely failed to account for the case where we
completely end the scan during preprocessing due to it having an
unsatisfiable array qual.

Pushed a fix for this just now.

Thanks

--
Peter Geoghegan

In reply to: Peter Geoghegan (#101)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, Apr 29, 2025 at 6:51 PM Peter Geoghegan <pg@bowt.ie> wrote:

I don't expect that this script will actually discover any real bugs
-- I ran it for long enough to get the sense that that was unlikely.
But it seemed like a worthwhile exercise.

A slight variant of my fuzzing Python script did in fact go on to
detect a couple of bugs.

I'm attaching a compressed SQL file with repros for 2 different bugs.
The first bug was independently detected by some kind of fuzzing
performed by Mark Dilger, reported elsewhere [1]/messages/by-id/CAHgHdKsn2W=gPBmj7p6MjQFvxB+zZDBkwTSg0o3f5Hh8rkRrsA@mail.gmail.com -- Peter Geoghegan.

I'm not sure if this message will be held up in moderation (the file
is 1MB in size once compressed), so I will explain these test cases in
the next mail to the list. And, I'll post fixes for both bugs.

[1]: /messages/by-id/CAHgHdKsn2W=gPBmj7p6MjQFvxB+zZDBkwTSg0o3f5Hh8rkRrsA@mail.gmail.com -- Peter Geoghegan
--
Peter Geoghegan

Attachments:

repro_forcenonrequired_bugs.tar.bz2application/x-bzip2; name=repro_forcenonrequired_bugs.tar.bz2Download
BZh91AY&SY�<�B���`0@��?g�����@ gF~���Z�juw5����s�s���m�[ww[,��3k��[I�&����1([A��wt�l��[e�v�9�w���w+��[����tC�2;j�����+Rv����mEh�WL���;�jn������P����E�M@�UP��
����j����W\��k��m��j�e\�$�
kk:������qm�s��kL&�4�a�)M�l��U:�-�eT�2���u�wi�]���L��$Z�����K�v��:.�w#T,����������vb��!P�����U���L�n�]�j�Qe�����RRN��km��Vh��D�n�����)v�n�wZ���l�i�[��e�U��un�r����
�I�������[���Z���[]�w7kf-�Z�[Z�Z��[;��R�F��fI�B\��.��2����f����Z��
�emg[�6gZ:r�fi�������H�9��K�r]��\���Jn��j�����SS[msK�w9,KX���jWF���v���m����6��j������������w[J�VSu�v��KV��TB�����]fl�v
��s���i���l��j�t��Z�������7wM���uwZ�M�����3V�k(�Y����J�[#2��.�[��)Zwu�W)���E��[��:����j��jgJ���]��kKk��7en�l��]e*�)�l����j�3e����4��m��;��.���F�t��u��+4n��"[,i���
���M������������sl��+]���n��;��]R������5��������:�ws]d2sgu�F��;�uv�4�Z��k�"6i.���*����n��U��w7Uk�uV����M�n�k���2��gU��Z0+;�N���C�Q������-�tb���7vuR�u�]���kj���
P�UB�j-[������Z�M�������X��p��v�����'mu���d;l[�Qv�M����d��uk�)��ke�b����vuSN���cF��R�������+-����1�f)m�`	UZ��Zjmn��v�n���k��k�i�i�����+��u%V��f�������ki���e
A����
i�Sw]��Cm�i�-Y
ern��K[���Z��n��m������c�
���&�+T�4�+����i������dD[-��]]��i�wn�t�;�t��wm�j��]���6�������uq��s6rf�.���l����������v�q��@��Z[�s�.�
���v������WGu�W[�Sm�v�2k4��mvn,�:�Z��;T����M��a���f�5����-��c�������K��Z�N��j������nF�&�tv�.�L���m�3H�]����v������u��������skZ�m-ww��#�N��3mk6����U���3w4�����mZ�}1�:�����p;i��������JTl����q��)����-�����iZUU�M*�\�JUV���%u��I@*����$�D�"TGX��j�.�*�YVT�P(�MST����*��VV���4�0����T��m�M�4jei��ZV�TUV���*Yj�������+*�m������Z�Z�����4��5�m�)VaD���J�����M��X/s�{��u���p6���Y����upv����P.X���e�*[
hP�UC��N�:��s�+ZVn���w7J)TJ�4�@��t��$]��[��J�������vwf���-�H��G�{��lh����;���}������6�����tQ�by��2drme����g'���Mm��Y�v����7g'�w���2-���6��{8oz��6�N�d�$og'�pwx4���glm�.�GGvxoxj�$�-�����F��x�.z��cN�v�����-�����5�>�m��������u��{���{��^���5��Z��k�jWs�Z������v�������B����u�^�v��UR�{8�=��
P����$��*�U�)xv��7�I-�g��G�l��^�u-y�7���{��m�[�{�����vv�M��7f�����:}s��^m�c�l[$�vwcj��
sv:.��vZ����k�z��io;e�l��I�r: �Js�6���'����K=��l�v��=�vwe�l��e����y%����d�f���;�=�-���l��e��m������}�w���������6X���
�p;����U���N���h�#�
�CYWwpT��s:�CW����vU�z�p�QUEU[���Q���U�UuU�������wd���wQ��E#�wf��f��2dv�}��L�9:7gl������o[=n�%������Gvn����j[��/}�N�r{������^�a���Kc�m�dN������U��&���6�9$\��/���-��F��;�vX�����E;����wg#��vY�};�[y�$v����rH������8�>����9��s�k���{��]��{��P���k��+�w��hkwn��T{��U5��{�*����:�w���P6�nQ.���@�`��� ��E*��Y�S�l�m��w��d�/�w�z�������-�6�����m������9;c�[;f��vm�5s%��7gl��f��;��v��W�����;�+���m���	��f��n����F��X,!�6f��m�f��we��:�
2sn�v]��v:;��g�w}�}�p�l_$���v���Z�l�4��4�����n�-���[;w��`-��e�o.	�A������u0;�pM{����JSV�������
R�Uv�5B�G
:����X�U{���5�j��xwl�E5���*��(�Sa�w7Y���]�������kfV�������R��wv�gm��s���R��/v�rwkgv�[v��������v]��v��wnq�-��u�m�����'m����oT���v�;�m$�Sr��5uMo%��d�f�������@��k��n��v�n��]U^�����d��n�d�wm������l�n�n�d��=��n`n�����p3�w������l������*���P�T��T,�t���P�Z��iT57�
�����R����	H���@5G���cl�mco��m��-��&�l��@�����nN���m�e����������m�4��wt��Y�c����m�������r������������]��g
E��s�v�-�-�����[�ZW�5���[e�;�v]���uJ�D������;n��v��5��Q���h���m���wP���^�����Km�v�g���n�z`{��
��
�����p=������p<���(;��i�T���:W����P��AJ��
T�J�a��R���0*�U�����`�D�H���ov������m����]m��.���o��1��m�J�����������]%�e;��v��v�Y�O_]���l��f�n�[i3*�X����n�]���7m���N+fm������n�f�Z]bko-�{m'm�v[n���wP.�Z����m�k[����]l��]�r���]�m����{�K����m���rwm�N�;�}��������v�wV��nk�7sp.�����h
���(�;�u��j��������V��������t(P����P���IH'���
��P
4���m��G�mz=�Tb�n��-����I�A4�r�N]��m�7m��{}}�B��n��9v���r���f=;c5��-�u��n����}��k�:��v��n��r��{m��wv���m���Z�m�������iY�[��)�l����n_x�E�m�m�t������C{N����om-�J�k>=��8����vw;&��������`�����[u�R�(���4���X��U����k{��B�������.��<�:��5�B�IH��w0�EAA��Z�V��q��v�N�Z3����iJ��v���N�r}���=+�;�N�m����
ih��w5���-��e�R��m����u�g���}���}�.�Wv��ll��h��u��m�n�kv�����)6�v��n�}����k���U�5��m�u�n������3Kv��Xkd���h�����0	�&�����`�S�0��*����PMUQ�����T�JU(
@�S�R`���j?Jh��
Q���b�h��T�O��~����
j���L���������~��9V���'��V�[{���7_n�S��[��A	���l��#<�Ot.�Nd!�HH�_�#���$��M����B+s7���<������2���/���M���qogt�xd����*|�b[AY� �`x�>h����=����7�.�;y	1�gt�X��@�(:{P�R��U�d+�S�-|��v�{zl���g6���E��f��K����G�f�����dvW���+�G�5���y�\FX����P�J���1���%d��^����.[�3��(��yh��P�]�����h��/��lr������Ub��m��"N�!N����m��!�fz��;��}������Qh������,rT��klZ�4�c���@��'S��u���)4D��!�g`X�j�����5	����a�D�R
mp�Y �+|pf��3]-�^�D	�����b����,���YS���)�"��C���
�Am�����v3���w84��r���G1��MV"j������A1�)xF�.����y��L0_t�OX}.�*��[��^���)T�*�P��-�=�K����]-S���nf�b�gL}��������������LG1�t����+�Bw�fjq���4!��\c�8-WP��7��8�z��b,���z^��	��9K6*�X�y�Mw�-��g��j����a��.�����u����Y�b�wYc�����Jw���'�_�^��J�M����rUX���U�n�'
C��AO-l�.�F������Dr��1�u{�����b�Tz
�@
������h�����F-���R�hT�Z�����w�n���5]���0~/����1�M��v��-g_��=�VD����bW*.��L�����C\I�I\Tu���3a-K3�����$��C6��������S��t��'�yS�P��]��������,���e.�OUb���[�����)�����P�W���Q�j/3v��2��6����Ob�F���yF���i��R��9�r6���������9�!��L��	�5�]H�$8z���(��q�N�&����bf�Xsw]'
m�,�s�������;WTI�t�
�9��Q��t,C��S2&`��[�����)qq�]������H'2�5T�������g`�r�D��l�����X�!�|�\#���+�{�)�kLo]$�ub�mIQ�GI]G+��=�:�8���[����x�k���s��(���0"���-�^�u6�����)��4k���2���[r�w4���6�6��Ad=���8)W��vuz�pz�y��^[�����]���w����������,�x[�k]�-+�J��N
:j�y�9�����8V9��]]%����4d�l�5	)�����rt��pLS}
plc=B�R��k&��{�Y	���+�P�M������N3�^X��6��Ui!���/������YT�eT&`m�SQl��.w�bO9uk�l�}H��L��T��
[4�?��dS��n��b�u�=|{5185��,�
+��]�=�����
c33���]�,K�6�A]ox�9����|H�[�|=j�Q��:�������b�U�`|�m�2��K�*�������x�gB��%%d�����M��9P�6�^������	���9��N��=Z��Ll�
LN�����w\�
���'�z1��B@���	���1$�7�U��T��z�S�����opj�a���;p^|yf:t9V$�E�7�MmK.WkI�C�S�Vj���s�����xvD�
��/�7}��\�����W���x���4�
��y�%�/&_��S��[����;pc����)Z��3z����4Z3��l)��G�"�X�s�H�\\������=�y��������'���5��C�P���s�Ls\�,
���������x����q�%-���J^G��6���*3�NX�[nd�s:���������������]��Sw/E���7�0#�:���&v*)���������a`��q�8���$���N���S��7�BV(_N��S���^^�w][S{����41��-\ec���M���g��">9
�
eAk&1�rou��tJ����(\�D�p�O^P��o�;��4ez������F�u����nw��P��(�Nu�0u��Mw;5���+�|EZ�=>H>�Z�8d��>\���'Y��8lO�z�C��W�K���5Y"��J6w=;����:����Mf�PQf�f�kI	�d��Y��Q�Kk+��������`oR�������N�cy|���P.�l�[�{���v����8	�\p����^��[��#i����1���m���\���"����Fnx��Y��9da��M2���nzn{�
�����P���t���N�tH�l����	����[}��8\�
�
Uy,���[�!����[�}8��A]�_hI�1��t������C���M��31����0�c�d\�/_���m�5xd���������U��Y@���q����^�*��J{[�J�W��.�1��_���1c� ��2�p��c_f��Q�����Kl�1-b����n�v5�]b����>��a��vY%���:w���������y���3��\������7j��r.��zK���r����K0-[#5l�M8sbC���
�����Y�z����kU��v�����Pe�rt+�`;�5�dbYX�t�n��Z^u��kuY�b�_2]�>6���>��a�+�Gf��������n�"I�/�g�8����!z��Q�"��4'JDb��wZk9����#Y�/^��D���^��Tp�"���7E����������:^� ����{B6��z��)�����"&��IB��>t,
�o)Dx�P�j���Ao-3y����(_Z�|/��+%;}:N��N��i�(Qk��WP�N���v�5���eQ����OR#f�/etr��Py��U�.�wYi��mE��qoN�{�A�;�qO��T�g���V|�:��ir��zt� ����<�A������!�k�4���T��*k������;�����!�V��F0�]W{5#fX�Y���X�{�c1�������<Ew������gpN��,���Z*^���������9�����,��P��F�*��azo���u���kc�J��4�El���b�
s��Y�7s����p����'5�|�����T.�Qg'id��{�T������������w�?VG	W�z;t_#���p�
0�+K�C�[��n�E�\��2������-^��},�U�����MF�iYJf��{S��N��������/��{!q�6�Y��Z�F�o�a���!�e����I��3�oS7�X���il��k�7��n`�uw���]��+zoV������\�A��0�'5Rm!]Q����1]qr�� 9���M���b��J>`N���S�����f�q�H��k=���n�6I�����9|�+�LC{7ce�B��iJ�[r7H�5�T��o�_[[�a�����L��
�Sv������"s������}��p�8F��q��A-\����"��3�<����7"
�W���X��qM�w�{�C�/�&��^G����Ce��?'2t&�?OE�+y�f��VvaOz���<�]���/�Fv���Q����8�WZ�����6��ZH��=y�q�����!�� XWP���Js������{�g���c����m����I>��R��x5������"q���cF��x�{{�
�9n���h:��O_t�' b���NT��tF�xT�9��q�����M�\)8 ��Y��w��_]���i��u�C;�'|��gj9�WAM��L���B�������Y>��cS�.�nF�;vr�5q;}�	�����R+���7�����G�J���{Z �����J���jJ+�=a���=�iw	���\�[q ���JU���v����x������QzQ]/��`�l_���W�&P���U��wKK�.g'YC��&��d�H'h����K�|�I�v����!=��7%��.&��q+M��}�(�bU���cC����*��uUZ������Mh�w�����6A��[�]9
����j����V&x!av�(�^�������<�����L��P�*��h���.i�����<B��{�����(q]UfK`�GUIH�Ju�;DL�R��w���y�n�B�a�D�]	s������yb���u��kU'%wt�Wh��j���d���2"��s�{a�;w��|����M�Z��cgG�|��{����w�G�Z,���mlh���Cb�3.�j���.T�g���pI"�*fm������c%���H�j�c��c��[�!�	�
O{� �>���a�Cf���}��)���z�`�2�������k����{YYnf��V���n�k���/�dVw���m��2�b�����QR��j�uTX��o�����m�S���f�����s)������"x�cu��[z�����o<8���WN��!*��q�[c���*�W}�jV�Vl�U��\�����R�$f����Z�GV�9N����B/Z���H�SO�e*���=Bv��l=#��z=���nzp�
f��h�>D���.h�dU���mD_��X���g����o���	l�c��j�G������Y2��S��.8T	3t��
�V=��WU���o���V�1I��I�m�-�bK$B�8���x����WE���^��v.�=��f]�����#��s�w:�w��
Dq��N���w�\�}*����.+`��v<���T�v*r7]ZFoJ��q�q�'��~�1����du�n+�r��,i3yv��/l���TmQ|��uD�������(s�0��&f��JB�f�����P*���k��|����#����hVu��n�D��9V��z��+q5���n3C���{��=�lJ�
��M'~w����z�����`���*V�������D�O2�mN��p `�r(�dF�g�� K�Uz`#��1:�,��"��diX:�������R���q��T\��L�����"���ZEd�Nf��t������L��t�:�Vg4���Vd�����:�����_���/r��P��k/+e����F��XM��a��]�"���H�b�q��oN�������
m���l�)�vU7wT[�a:��uC���Q�&�3.�i���s�����X����i=9�+��
<���������2�9`�������=�~��~{i�Ut��SpF��,��q��*!�J����������[����Y0�j�
���������3�4��#���@5����z�L������g�b���m�j��N�-�_�"��B}��|KD�3�co��S���,�V��d&�i��hS��pg�xL�F1�����?S��J��X�k�����Y��M����e�~�������'�{�{<�o�+w����Z�8u���RCz�Z�3�:�X~���;�]a�/+�����	?
�\��$z3/�Y���D8�[ ���JY��r��q�q�o�U�@�����������2b�����f��P=��)*���Z��)�h��p�����2���wp�9������l�n��:�2\�h���,VL�}Hj�(v�������"�:$��^wu5R=�B�Cy0oFG�q"7��:s%����w![T���e���C�my}�C8F<��C�U���J������(����Z��-���P�z��Ou
'�p���W��j����o't�����|�y�A��C�*,Y���&��K�
���D��o�V��
����3���B�0��K2�����;G�����J�R�������7VdT�V��Z���$��<����um�J���Z|@��Y
5�XB4|��sF�|�]�xi�<5�N�>Z�%����.�\0�Z�����#/&�TRLqOJ������cp������u|�f���N��	�(,�U��U�~��+�u��n�)@��G8�C����i�(<DS����\�8����!�c�xx�<,2��:��m��}\v+,k�����n�v�7��5�P+S���,�$\�ng(*'�@V�(���Ea���,\m����q���,�~^���pG���K��'b:\�/�������3s�p���q�a�cgc���S�]����7G{�=�S[�����U^���n]��iz�Mv�\o�2��{.2t��\���������f�|�������(�n�Z���x�xj��p;���J�g��y�B�c8;�x������h��XP9���^Z���/1�i�z�s[�-�Z���O���t��2%��������P4����LQgr��Dc��kSvF�j���E��$��H����e6�M:�W
��^�1��=�����g��L�����������;���5.c!�*�1N6p��ol����d�"Mwt�r���Jt�,��I�ql�`���>]Iz�����Sj	!�x�C��R��9�Dr<�{�
��QD^m,�]�5�=��}�����a����(�+�'2���k�)���%�����y�]Q<}f�)D`����7�������d�|4
��V��B��a���c8��*`�fq6��F�},{]��.��]b�*�K��b������R�I{��DxR,h2Q9}[�'���jc���~A+���������!����d������t��+�V4����u����Mb-�,S�j�i�H��v8fv!{��j�hEOeq��Z��&��{��i��(hc��Wb,zL�5�5+�5�d�u1<��g4c�f�kr�E��{#��S���L��z��eQ;x���#%s�;+��,�V��Sy&Ss�R��&��YL����6���x>�>��!/4VbX��������~z�������N�R�_v9���kwQ��0y������,�DGb�g����@���
�Uw@��X�3fL����K���g������W`��3���l�E��5_);�".�m�6�q�+iaD����l�
5/�a����LWK
��Wf5��:�wE������^���c�C�h����Y��7u�/M���Jy�W'=��������+��Y�B}�pR���-�oQy����dX)2���'���\��
@���sq�	�P�h]�X�lU�e�p�x&s������^��>�V+�vL�D�;��[u��A1��H�[8�h`�M�4s�KN�Ll���]������tH���\l�����0KkZ�-.�f��#c�U2f���\��*�0�7�\�%�WJ&�S��/,�|�[i3p����f�&���6�a�� ��~������-�����A��&��:���rt����t'x�)��I�6����d9���V=��(Oh����pN����ao�S�7���;c��i[�}X8�~5V��[���n���8�*������o,��B��gS�w�����F/N���\j�g���|��V��]&9�3�m�)t��E{;�9F����p��z����i�W|;��������__a/&��a�U&���*>r��k{�YB��8��[bI��e���KI�{�s8��B�<�
Y{��0��������)��{)<�W��+z:�]�rk!���2��N��.�O^���@O{�{����X=�7w�\��N�pN�{1���d���]1m&S�O�[���f��|�����5��^8P��m:�0s�Cb�T�T�n��Y���B�A������!�X9jAG�}Ou*�{n^�
�^����f���gdM1.�>����\{���h��Q���;���p�J�����m����.a[�����{jg���+�$�v"}��2��*�0R;0;�'j�����:���%�.�}������y��0�n����������j������_����H���m�GXV���u�P���y{AGmY�9��56��0����:C�b�����u��S1J"�
��2j���]�`l��3z��������%W��A�)��P6�j�
g3}��ys
^��q��SvuT�fa���������.W�X���Z�4TI0�Y��!�1{1��|�������n��]�u-������P<�mJ���k5����t�1v�R!�d�/��%�v9J�����������e�pBu�{t�������p�^�	���[��*�g�G*����H������������e����t����}�nl�u*}���B��.�����
\KW}U.�E�������(`�)1�wb{�����Z�wGU	��g7�6�1��;���Yz���|�=1v�����`3�^47�&�����`
���������x��U�4������0�����^9�=�����W��VP��-�]����y��
���rx�`s�����z4e��v8%��c��q��?v$��^pZ1V���;
���C��8�S�S7��w�y��6���c���;������n�Zq��q�9u{+�b��G�9�Y�C2�z_��c��v�����=|[t�e:^tN_��=��D�������7H��O����pW������T�`����H�D4��a3����FUg�lj��v�{S"�������9b�Z��DDu:�k�����9v�k����8i�������
9���SeBR�-�U���%x�u�n�w�gL:��i�2��f�qk��r"��=r��,���T�~���NlhR �0���cak��R��\6���&j�P��)7r��{�4�k'�AdO�j�z����jN�\�	�K6T��&/k�C�d�������^A��Y���s�������-}y��l������Wd4)��ob��g	y�r���'09BfT�I��gF�<Sl���u�41�y����U���w���`��QO'���7�KO{�A{���X��?:��%���8���VX�\���B����m�j�����B��������V�7�j�-����F;���h��]Kn,#!%���z����K.J���2�����53|
SSD�U�M�
�1l"q�t��/���H��|��Gw�c����I{�4���N�`������U����a\,V��&%�|�]G:EMgV�q�p�U�c����7*48�������]�������6������N:��0��5t��M��"����������=�X�Y�Xzf(Iy�eE37������M��)rm���/m�|	�g[��T*P�s%�R�������o��*��nN'�������N{����_m��o'�T�������E��
F����^^�����r��L�O�7�yUx���6�:Y
����� �fQ����\-���l��mE��Y2�k��h��3
G-yq��w���t�@0�
9���L�l�����z1���{:�2X<�}���1��p����'L�[U] TJ���Y��N�5y�d�nB|��'�,]n�sV�/K1������c��_+�7�������P��\��w�]�2�k4Lf�JJ����)eK7^��5p�������{Ki���.*�F+����sARl�}�R����FdXh����SKq.)��5������{N��B��TMt��[���`�J�]-�,����������U&�u��m�t��(��i+��k���e<���^H���_�4�o�C:��\ZY�����u&���8#�^���Z����!Jk4'\��<�_7 �r�����VB]�BN��������E�c��8�-O`����]7��JB�������#!Ov���j<[�2�����x�(;_��/�fX��IsQ0��2k6((�J�J;U�M�+��v%�+0n�[:�7���]T��pw�y���ku��o�57�g��b>����~�U���<������uh
"�y��6�,����������P>~[�z�-�e���&*���Vy������?�'���Ob��n���,�����K��������"3�[6�G~��L���d�������H��y]����tm��$��N��d��L�����h����|�/3$�%8gvU��gj��� {����2d�'���w����Q��t��|�]3���U
���p$���4!�,�c���
��	=���\��W��������m��p���R�;�+�k�����B4
��%����������'�3iN�e�.�}���d����M���o����{H{i{f��I[����en���������G~%-�:�T\V��l��s����[���W3��PR��t/�k��xvn�Fr(�Sk{�7x���T]xO�z�]L���� M�l�P��du�� |3z��Qz5�|��eo�����T�
�������r��eG'\;�_/�����\�u��}��B�V����UEZv0R[�9�m�Kk4J(_Y�y��^������I���k!���QX&��r�8S������XS� ��`F����O=O��o��:��y.�n������D�����u����+�^~�p���wGW��=8\� �<�����g|��[q9�[��65�,'QW;��ed�Y����<�44��;�<�I}-{E�(Bww]��50L���V0�����@���w*��|��U�������������(+��>vE�: ������g4�����!�04���%V�w�m�E���Jg[��k�xYF���*���������mvn@]?a��~"�f�]�b���n�I��e�l�9��T�b�u;��}4��_^v���t����\��z���������7P��v�R����>X�jkjS�j�;=����_f�����xyPe�����Nja�����Wyz���|�S"x���If_��c��pR��S����5�����XlGf�.\@���UbR��C�:d5��$IH\��boY��x�yI~�m�w�P�w
���������F��aY��8f���
��nj��t�*4t���"T�����5��EG�f�o�����m'��O�$]{����)�G5�}Wcg����S�
_^���*u�wyuB�e���v�������*�_�}��u�H�sCv���od��U�;�3�y�s5�)z��p���������������3(��<_`�]��l������77R�wLv�����0d����g�P��o�dmJ0���t'������v�y���=�z�������.6%�/^����V�K�{��h���YPo���c��������0>����&3����`��L5VP�|5���j�<;-{�7b�Jb5U��}��:<*��8n������J����g07Dje�nJOst����jb��&�u\W��`��cf�6'3��O��/������*B$h���uyW��u
�f����Z��<�'�������;�wAONg:���,��fb���+f��>Z�.��v�����o<�M�k��=EJ����m��Y�)�����E�����o=Y�I�A���/��[j���M�n��r�\r��������cY(����J��w�G�A�8Nv���U8�zN#�I���y��P���������>�����Gh�L�vg�n�7HH�xV*���I��;���VNa0@����n�+������74o�:�������E��N����	�37�,������������-#
��,���Sf��w{:����v'�5D�e$v����������1������:r��q�>�b1{2�c������d�Z�pF�I%`z
�k�w>�8�+��b�������Q�SA��`O:����z����8�V\+j����
��������K0��7��3�T�]*h����8+��Q60�V���yVW���xmm�>�'�#}���g�4�f�����h���%�":�$,D��X�m�������]0��{N��Q�d�(8���z�XBn������.�Q�n�f����� ��)W,��$#&p���4��[e=�A���w]�D��������.��!b��_�����w����+���ZN��$;����}�u�������S9�\��$�w���;4j��d��	��]�����[�1�!����s����Y�O�p�����v�[�{}��N���+������,���
�Lj�������=�-�Q^�|��D:���;i��g���d�l����nu��s�M�q.+M���F6�������pk��p�BN�m��;����U�2<�R�<�N��C����:��k-�,����A"��va�h\*�����N��Ir#d��b�(*�75nM$T�����S��.\����rq.��xFS�����:����]�m2B|hzM�W]pI�)���h9��rh�`{��1�P�&)�����'
�����z��B=�8s�=�:�~W�jQ@��"����r�I�6��:omN����
���.���4������\����]�:��W7a�M�������A��w9]�y��7K���jyF���\��v��������C!��L��W��]Z�z�g��
5��0XV��PJ(��WU6�����,k�}��F����R�ufSr��](b����*=��ga�Y�@\R��kuW�]�J�n)��vL��a����T5��M���!S������0��4�������[sR����9�4=�f�X4v�`���m�[�Wx�f��1����Z��~/�{��@����}����� ��.��~=�x�<#����N���w2���`�t�V4I�&�j	�]��.����.����T_
����
�y[�;��y��8L�����D�a����z��}z�&�]�X�b�v�R�T�7u�]�T	9$�FFX�g���..��:�1�Ov���TO�p������oE���tn��kg��������5��p�(��esVe���p���U����c�`N�-�_*=o(jy�U�w�"L^f����y�������������G%��Z97<p���F���^��^W�u�PA��B��Wx�����������g�H�P���+���fn]P�	V(z��)�6�V`��|Sf���W��� l-� l��U�s�q�����n����%��"2V��V���sTw��n��:�s����������0rs��
���������tX�V�����\���t9,Y�\��t������B��x+�laT����2�\{-��>�Q��w���sO���d�E��eJ�j,��V�������/��j%�` {_'��Eg�2�.j�������c��DY��.��x���e������$<�������:�j7y��1�yn����E`�X�c^w�\�V)u�P���3y�
�����4�<�\wPK����MR�b�*Lg4�aov#W��������(������d�.�A��Mi�U68�~��`�~���J��
���%9�V��=Iy����]k��XTY{t�S����t�
�3N������������K�+���W]������y��TP;�����o����@��E�
�i,U!�d_b=�o�d�����qV9�D�sS�}�uQF����JX������*�n��BV`y���g@������*�/w	;�F���X��:�N�����pR�r����U�d��g^���Ql+>� �]eTI���N��\{��C&�/{���w�&��[Z5�'<�n$���F\vE�1������;N�*���m��r_L��Ur�����2��9���U��Z�m��3n�>w�N�uu7��(5�q��[��j,�!�Mg�6*��N���.����+.�m.����5�E����0��U3�+���S=.�K
��-o�����+���B�~���������6VF��i�yc���L
��xL���G�.��W��@w���������#gY3��rT���k[�9S��j�/�J���{ynj����A�����Mq��u/EJ�^e��YR�����2���go�:L��#93���@����}(K7K~�`��%j�%<W|3 ���yavj�-v+���w�����O��vS��Z��x���k{�v4P������F�:u\�+~-�qXJ���3H��w���W�w�l�t��9^�q\��4%�<R���v�C�o��X�j��&<I���uV�����W����~��y�}`��5�6��������}\�r���y��X����e�t��2o��i�u���f�@��
�����tf�umr�h9�]�����p�>X����x<vb��x�c��d�hD���&�w^�R���
��5%c|�}�]?x�~^9�K��&�3U=c������<�9����N_h���o�A�����S��F����.@uL����
aKi^���������4����K��1�h'���e�����UJ�Z�z8\���(�g��
h.X�N�.���,��t��-���2�E��F�}PFq�3�<:��/�����5'�$������G�]r8!=��
>i������^"_^���C��z!������5'�#�i�%����}yx�M���x��W����v�����+;����Z�N�\�K���G�e@j=�!�r�w�&k��O�������iFS������,b6=�J���m�p�'��������v��zi@��f��&��j��2�����	���B�%+��T�����F�d�����	FJ��c��2`nO1�)i���[��d8���k�[���<�/���1d��B�����N���}�i�#`*S&���u4w�&����N"�����
M�( ���R�7��e)�����a
�T��F��#B�^��:�&6Ar-#����������z�LC��T�%WSu��(�^O�}����F�j���ef#~�i��z�����0�rk�
I���ir�/,���>����-z����v;\*.��GxU��N����9P1���R�=�OA�:��\�4x�y����(�����H���������w��3+�x������!�+|4/�JiZ�wX�����[�,��T6�\�*��{�w2�*����FTygN*�f.�:�!�n.�6th������]���s2���s�:;����s��k\M+W�^NQ�}E�4�>��C���j���|��*/q�)P�N�GWyD�v`�jL��lr���d��w��P��m;���m��L����j�,;�h�R�BjY����E��!����{�_7x8�S"�4�����m�s����WWC�����8� ��*�Wu�e���	�W�����b��O
4�x���e��2i�	���;����(�Hh�ro������v�4As��u���Q�������^���U�Z��#O�
!��4�4��v���������:�M��wZI$`��c���<�E��/x/��N�r���x�����X=wbx�r�WJ���/��2o�r{�1����o,u�?sj*L1(Q���Q����2�~�Q^I^u���N�y�����Jg�N�H,��t����/��]Zu�l��G^m��oS�FY7Y���0�\���fp�,���w��D���0�%�1���,E):��]D�	{�uv�W�(�?f��+������f\����hi�O�uc(\�J,�����������v������Q��vlLz��r���
U��J�����E/iI������&�
�������+���T�Vo^�s����<�N���^�v�R�~�N����'��X��#�3Zt��� 
��}2Z���;����SE#���I�o��a������q����j]v���1s���2v�J�����ZEkU���
,�x?�������5����'�������Q��
��k���.���Q��t{��g����s���=].8�\{Y�m�������Tjf�j6��OMM�p��a�m�g��d���D������4��>�U���6����D�B�h�k��b����^�<y�T
��h�!�:�����"��*d��%D��.���3^xpL�Z����4:�Wj#�&e�.'�{�,�u�nP��f�����a����I��"�m<�l���F����K�)�n<�>��SU�X����UO�'��;�d�o�}�5�Hn�@��)~����{w�W��(:7X���+�{����3TJ���c�6�D`Q��-/`-�6k3�n���I��4h�7u��5�<OD��J���<5���t'.���8�
3���������X�'wvN���
�oq��jv���D��*�����DP�U�nJ����-W>����5�w��:�\����wH]���{={��w���)�b������m����{��*Ad=��C}�pzx�vr����c._���r�X�>c ��H�;�HTZ�'��W����������v{�X�_�k��.^7����a�i`�y��{��������6��w���l�W.���I�z�~�8c��{��T�o�:����r��N��`��m=�Lr�qVJ���u���/*��G3t�������\+P��o������y�R�
�bq���(RL�of{7}I!a�[�������_����=z���v-������a���h���m��'�r�X� t%����S��m����bs�zf���i����IK��4�b���Z��G7F3E��{�������=��RY7C���^��'��!P������=\������K��{�;���������������T�'1�Yp_��KFu���f�F�P����#&oV��&��Z~���G�)%q���ix���o0G�`J�:�V�Y4�����{���gO2i��%3���]t��������a�m�L��+��u��z^�z����%(W����ru����x:{g����Y���\�P��f*-��Rf)�0��]����:� v���%�Wa��o�c���K��b�.��4�6�PVI:z��A��x��z��V����Ug��;t����{O�������F:b
������T��$u}������P�Mf�%dR��������%f-�w��;	b��(������J/�K�v=���`����t��=���,����t�;P�Z�<�5��-��C�����*�u�r����%M����������0�J�VDyx'�{0�M���$	���i�HW����I��u���F���.�x�1b�kt&��m�P7��~��: ����������Go��������h��{��X�����p���3�2�����8K��z)a_�i�v��b9�S��������YC8�kJ��2�������lZ�NI�cyl\8������X����]b����C���v)j�e:�vyF�$L�u��2}S���:���jsT�gH��<��]E[�]f�@���
�wE�Lo.g������]���w]/T����p�O]�c�N��]7����)O���Q�h������Qa��"�i����q[8y��������Lw#R,#|v�E=����n��=�N��x3\��/���!��/8���/��)tkV�q���*p����������|�l������5��j�=m������8������G2�Y�g�/�d�]i9�����������o0��������v5�&������].z�i�E��[���_/�3-7�s��O�y�1RV�iY��-V����P��5��g9n5�y�%\d�<��U����&�C;�d�r�
w��N ��1��qW`�����\�q�[���a�y�w�}�L����x���;,`�b2K�	��b������~�����,��:�;��@�7�r�s�Q���{'S�~�����<|4V�e9�*�GV���
��e.����b�����]s�\�k]�0wdzM�Ng:���{�,[Q���;���O��r��R�
��@9R������������a�j���Q��i��j��p5��2d����\��7�
��*{R8�z4�W$Y�� ���lh�My_���b����8�1�y�.f�	�)]���<
�O�et�[�PDv�.���_-	�:��ew�����[;��-.��n�}9M����LUypX{���a�U�)G�[f��I"V�v!��^;j����,��9�2���S��������a+�n������&����!�����y������wbql2�n�������e }����c�^���&W>�"��.x����@f��E2r��U��5�u;����c\D�wz��)b�S���t�Fs�_*b�\o*;C7�'0������x��/M�LM�����/2���xO�'*]��6��o���0�$�!f�=���[�sx�K���*�zZX,
5b=���W����lI>���y�K�L�U�5�'�o����>�I�^����U��:�VGM��H&{�Ss"�{^��y�I���@�pN
RH���.����
X�pl�{��F��R��*	�5<�7�k�G%]o�?S���T�����]eD�����P�f]�]������lf�p���P"���Z`��n���b�pF�����Y�8m_9�<f=�Zg�6�~���F�-�:�]vs�i��|�$i��.�?7��4�aW��kv&�d+��	Uy���qvrXH��kx=e���&�P�vc�5�Z�w,,�e=�_��f;���l�
0�y�\#��Q���I���mXY�=����0"1�G��p��9o��{�����"V�!��}�<��%����&����v)|Z<*�e�����S/�um�;�y]c
�t��BK.�v�����i@�������Wv-�.a��[�u�����"j�3����'�X�wr�7��p����X<���Q�~�~����]��I���4SsQ�zE��]N��L��}���-�~�J'�-��-����<�8/y���x'��k)`�C����������@��HX{�ni�Q�r��6,�0��y�o&{�u�s2��+m�c������C��V�����)Ym��[R
���:lFJ��+2P�������="�I�n�=P�H�f]�������
��t��k�K��j��,�����J�:�0�5q�����b�U���yk9A^��64�c�We����������[JM���3�iW�_N+���:4����r��i�����	9-�
k�am7�]fE5�P���_����_��w�
����T��X�j���2����w��UD��X�;�kuJe�V!Y�m��z�!���t*$������a����/%��aZ�(a���k��s��d��}f6��i�n6������n9B�������j��/gv����=h7R������'6��D`��:��.j��f|[�������lT�@s)�����-�@�Eb����i���:W>]Uy��l:��(����5toc�M����==,u[�<�Q��$���n�����
�Ut�6��h����/�s�b��[�-������Vj	�����s�~L=���0��
D	�Z�Mz��M��J�t���6�'��xZ�SN���%�~�w*�'`�C����,*��\�GN�_x���f
T�gv��_yMS�����uo��K����s�|n��y���>lk;i��������)��f�c�A��R�Rz�7���q>����)����fV���_X�����z/&��{5��=����-y>���������.7Y�(��gt$5���hgU���B�-�.�,����z����q����������/�M��� ���
]��rK>���S��+p�mT�s7
���{��o�{V�(�q��)�F��&�h��~B�m�{^K
��������CBe�Yz�5\�od+q������{��=a�4"�����L��63����������=Z��CM��u�����O4�6��
wY�����[�;����n6�TKU���`�1�J���������l��^h{�t�C��k�������.�:����<����)o�;�6��}��NH��kqP�[q���[������
����O����������J�z"a<�e����G����^a���f=bA��35��������ZJT6tZ����g.��+bc���w2G�T�6��+i��j���>=W\��K7a�4��b�sj������c�6W>XT���F��r��A������y^0�k[SJf����f
���%x�����-xC���`=����U���w�[�������R�[N����[������k1e��R�*�)��o�>/s�W��|�a@�N��h:���"������8Q����nW�i��������t3�n�;I��{�B��1H��n�w8�gC3�	����B��,����x:����H56��x����7^�.�pk}^���&	������z��g���]�sis�fn�ui1c��j[�8������j��[��2XH�j8��8T���{��Q�����{�Xh/w���^�8*���SK���hAT���d8{���$���n�ro������tc�]P������o�����IM��Xc,
4;��hm�{��(�n��'���{x(�Mw���M�7�u��5#�������:-���.�������IJb�_�p���*/8r+\=��s�/�����E����&�T��\&�
�D�<����;Ik9��[���.����C��B�jid�>��-�S�:S��m���st�H����U�E�$�6��j]B)%[���c��m�8^���&u4��wD��K)�~���2�Zo�/�8z�{��g0f&�r�������X�A�B���.W�0h��
-Mwh���r�����2]wT���x����b�����&������:��R��!Gu&OK�u�w�1�o�u=�fu�O�M����}��s;�r3Y}����:s�wz�)�������p��.��C��{�A?x&���������2��gP�+)RI><H��
h��1S7�N������*n`���WPz�1�&W���@�����/o�{��c������;sE;s}h��=�KA��R�1`V�����N���+N���z�6� #�t\�w��f���5���G'�+������,�lg�P#/�;���xT��o�|�������\�p�N��Nc"����Lt.r�����g9=�Sp�u�AQv��][Ub��b������;m,2�r��\�>Z{3A=�������a/���f��=w������T
���~\7��~t��P���K����}8�k�,(�63	+����uJ9������kqYkQ�`���}�QU#5|�$���L!D,�@�;�U[d�4m�o#7�����&��Z��;z��0��������gu:�W��p|wj�_j�%!�sF"K��d3���}s��c�>��F���w����b��j���Y\wk���J�\�O\���<����g"��M�����c�xZY��J��z
0��p��|�����8x��w#��V���*n�>�~����H�LR0�u�fP�������(,�����-�����f��Yc/}[��z����z��kUn��=V�W���h~���#����o��=�Zxq��|v_>���l��N�����7uZk���+
�kv���1G��������_�a���{i�s��R]�Yz$	��o���� ��u9%A���h3��ec�c�U}r��g<rA�e��GU�s�z���n��4���v��7���"}�f���x���&�D��J�����A���!����:]���$;�������:^�n]�u���f�
 ��
���47%����lgCn��pD'uB>*�;�r�v�4�o�<M�3�V
�,�R��Db���2��J�5�Su��p4oeb�����>��������i2�/L���2 p3��U�]�;�1����?e���_[�1]�s	����rA�h���A�{�����!o���Vt�v]���%e�kp���s1��A�������"����:��t"��UV������h�}�]��7Ca�l.����������d,:tJ�/��9�u��������}�����8�m���OM��	d ��Vken�6�������r����CR�+;��2�v��8�]�'�%p�2u��UyK��j+�zs�*�y�J1lO9��7@d������m�J�v;�D�[�xW��C�k�u��dK��8��*3��q���0�v��a�����zz�wi..a^����-_�Y]�!D���^�&1:=;c}=�5��K�7��y��5��/]��I[���Q����(L��+�Y��2�+�c�m����P��_�uU�j����
;0���7[�c���Yon�M�_]���>;�k�;��u0T.��� ld�h^�o�J*�urzp��/0��������y>����$NKbd������9���U�)+��h-��t"�������;�U73_V�u�0��int��~�Co�
[eS�;c}�e���Q�1A����;��,t��jr��.�b�RVRq�3���x���j�)EZvb�����FD�y�����\N�+o�`�����w@���9s���I:��|�GQX��/�f�o)�8�Ykb�a���z"G%��Rn�����1zH1!R���^�8����eja�z�n�g��7&��z�����$� ���+��s;���~�����8��W�g����o��u��`11rA�qy<V��zo�4���D;�����c����_�*�����*���3i�c
�)������u�T��
���;j�d������2�f��:<Y�R����YS��Xo���q���TM$Oz�<E/a��oS;������e|����zJ�cyO��ez��S�d���w���&�v7yg���g����9Gb�{�c_v�����"�g^�Tjl�p^�r��2>�nm������I���P<�gb���5����*A+y�0��d�5Gq��nn�-��{��Bs�$���_pq{���\6���BHZ�h�W�����D���S��QG�	��9���E�.{@D������<�dy��������X�O)�nIV_�1�u��=��]�Ik�l[8�k9��8�mu(����R�A��J�y��=����������+���{�/2>/�HC��+��(sY����.,N��<0�r�*�!i�4_.l��}�Z'aKdZm��4xX6�MDT�n
�M�Z�������V��m
�V:+i?X�K�Pt���s��9�0�7kQ���]���uT���$��5�9���_:Fa������{�� �}t���-SZ}��&f-�a���V��M�Gt�,�[���Y.��8���ZG7����{e�Zd����p]���G�a���u�W��v����+�<��|=��y��j��6�"�Y���|�H)�8����i���`�q��6;�i�F�^o���U���HKt���'���BTw���
�v�o\�{��esY<_l*����
��.�-��~j���sH����8�@���,#�����ER\4�`d\}�'��������:�7N�Y5���}Fb��yZK��h��<���������U�Y�d�93��Tf-
����q�|�e����=��hh�W���#{���d
�w�&	�fz��w����T.u'��^�kB�t2$��{gb#��{�F��7l���h�����s�����%�t�u4�1-X(�����*��n2��mg'�T�:�i�{�
1�������yu!��`�%�_%�D:6b=XUMY�Az���s"���(i������5� B���F�N�j@���u����%���&y��k�\��I�S�mdwu���r��[���w�����}�<��E_{DQ���\H������g��������:��"���i�����b�����dZ�&x���F�(Px~�� �nvG�T��hw2�[a*��K����x�`���5�d�`H����o����5������0������!��F�[{"hKO`X�������s�;��+�29��wg����Z���+kmuqy�\�����]���<���)�C�o.�II�tw������0�dyN�SH�W:9;������;���p���K�*h�b����ScQm�up��]��T��X>��{[�����p����u_{���l��:cRM����g^-b��/p����\�>u�����#U�MK��v(���:n�������wH�*����$�C�����'9)�~�U�����+���Y����R
�_�>������v]�us���3F���KK9;t�Q%�!{�5T���h�+N�BZ(/[��Y�E+����*>��SU���Z��1�2
��r��b7�����e�}{f�����{�[��oo��`�H@��D���rte`��S]o0�]�e+��-C�	Jk>��QDs[l�f�GO�e.h��2���r�
��\m��W[s���[EJ��]y�]��X��^�
^�����z1�7��4b���=Y������!}�M�����^���?�a{�n���Eh�����uU����������y�r�k���"]i�i�I:yl��(��Q�kOs9���.9�J6�(��Y�a�p~�=[#�Ib�T`�����e{�}��{�d��3S�;+���}S
���x��kc��7~�
7
�������\u&��(��"��nzn�|:������#������K��x����e6��u���SArtN�+i�����bB���n{t��4�s1�c�����e��zbJ7�������"���+���C��B����*�ZH������(�i��AR|F�ZV��HV�/9x�L�z�,N1�����6b�!�L\5&tc��]v�$��
t'���EI�u5���t�����`�����.�tli��T����F���>�:�y�]j������'r��`S�r��r��d�vbl��	p����d�N�
����J&u����K�%�Gx���e�CKe��Q�S��a���� �}�5	����5�c���'1u���^����N���#ph����j���?yz��lE[�i!nf��l�m���{u��I���D���.�Z#�mPM����0��r��|���8��hF�3w�o��v��Z�����8Z3b�WS��ve
eLv;���j`�,�RZ���2��H���J�����3���s�Iy>�
�-�i�7,$��.C;�j;����^�-z���������j�����l�p�G�[��vA�g2������*�zI*=F����uZ�i ~�.��[��t�b����L^h�I��`����{%��s2���)�����}�H:����_��Jk3O"[\�o��;Wu��N,��b�7����]Z��	����r<�-Ua�k���]���\���^�����T�v!�i���o��I�j�Q�
���z�c���0�J#y7���^�����.}�������Mm�@D<m�M9�������w�R����:�y����9���Z|������l\ox+���8��c�zn��;�� ��R��^}�c�t4iK����������V�������3�����m���5"5�,��������u�,�:d���tz^��	wY��TG��g'b������{�&z��):����NKU���������o/a�7U�gg]<�o*�r��Izj�	�����R�sK^�M�E���
��M�'K����#~�Dg?����y���\��R:�.<�j�L'��r�w�Ug�C�G;s���%�����M��"n�s����Dwe9��as���
����4�������D���d�o7}�c7A2��&
�[;�B�����{�/���(;�nz���z����`�y����X'�Er�w�r�~j��0L9��f�"e^�8_8��m�����8��}�����(�=u{�!����g�������<)��9��\�J���.����q�5��Ov��&�i� U�@'�lO���Az�M�U��:"���jcMos��H�����:�W��)=��mL�WE����s�jY�)���9����D���=��/P�k������N2�	X�9���1��;�n�DC/H���(�7���
�9l�����f��U�(�T���
����
X��Wgi��W��V���u�=���b��FM�MA�.�����J���Cva��\��q�����I�C�Qtr�5��+]	�������cj�xM���HN�A�
���S��6,��K��`�T�WAi�&��:6Y���������q�u���u�@�)��+%^5���%R���;�CU��G`�s{���3Yr�n�����b��^u�A�+
�z.5��N���`��P�9��u���_	b��T������.-�d-��n���y6����t�Q�j��Fp��8�����������3��sz#O�qh)�������y_k/KM!���c�|l�N�t2p����>I��2^p�*O���,f�������<y=�J�rVU���p�����Ob���m����1�4�Y���]���
����;.��G�(veN���V�P�e��������f	]]-�3J���H�v}9`;�	�v%�ftY	��t�[��Y&�L��HU����Nq�����������qf�����������q��w��iot��h�5�����j������K�������
f/#����[�e�be���`)���+lp���5i}:lesC�0�K�e��{�r���|��&/����q��L��x����H�����%w���K��{��OQ*��*���&o�@��R���% �Y�����d3j���o����F�����J8�A�57�9<^*����������^#�|���V
eS�`���Y����P_DN��%b��5.��7_�Q@�\�zk���sY�7����*�LOp��#d�k�(Q�����X;q=v���������D�u�&�'�d��'��~^g�K
�]��+COP�.�mN����`���P�(��������6g����<��b�|&��=C�i�`\�.��M:M�ee��A��������+G;m�+�c7k)���n�<�l���iPm�zo���N��:v[�i�$7P��u��x)z�vxi9N�a�&�\,-KQ����V��Y%X��t�[�}&sa�e����-q��g���+=PN}�jen�\�s�-�IV��cg��\=��Z�����Gu_������s�DAn���h�W�I���9��-���c����9H4������������z��;"(�CX������iL�&/nm�^4�0�T85��z���)�����
f�7��c���B�\H���7:pk=`��\1��F�������v�mv�dk{[�OY	
��5��
ck�D>K7%B��D���tt$�-���i���5�����g�d�������\���Y�h;��%$V<Si��UfV��3����QP�Y]�B����ZO����2����*]y%��w����3��KJ�A�X�@�W�/pG����7�'+r��/�Fc�5=K#k=^r��f,�
^D+J��{���N�"c[��n%�I�	����Y�	30�n�vfk����
+��&�sq�`���e[��[z��Ns8����Bqj.{'&9��|d	~Z�)�00;����>U=��
�+����G:���v�T��+P;�������H��\��>���z�P��=jQ�0�X~����n����k�//�8��[��X]��H������
����3kd�=3�Q�s_X��2�����p��u����2���EnQ�m�=;WqX��_���t{�������Oi��Wc�n������>���Z�k���g���5t�[�BBT�
��<�����jN�pnb�"0��"���1�k���-�r��;arM����2��5D�S3+8!�|��7e}s��k����E�E��������sH��B�Y�y`�sx��<p�j�����gq�LZCOt��6�e�B���o�&�������B�z���.���� ����D�b��)]�N;ec�YS�������(ed�6���4x�!�jX���Vn�����r^����W��	u��C���*��Y�]�5�<)���\��,�����f��Z���"j�Q����D�p6�j�:f�B�w�g>G*�R�j��C�:u���{��}y,�K+��H�"�p����C�����G�_��
e'PWQ/*��O�����k�rJU��9t;0]��4�Q�Si^e���\!B�M�����vp���c+��������Lz���A�^��H	)'���r�MDI�xq<88l��C�X�!0���lV?Sb�s����j����;���]���1t��@D������EZ�V>x��2�X�y����k�W�Z�Y�����g1{zh�\�v���
���w�/m�A��z�Gu�0e�L��X.��O=����=�,S�������m+�u�6"����CT=��k�k"��]<}�Yk2���Y���������_�CfdR�M��?>(�/�u�����'��k,5K��x;��w��
�X�<���
���3s� �{6�I��3��"3�W_��p;ht���K��y��e	����v�l�9���P{��eft�
U�8,������Nx�g_[��5C3SX�Z2N��[�/{T�mb��G���b��O�����aC�1�!.e�Z]�{�>��3�_nS\�r���8�B(�����=8i��z���0e��
��y�����eG49Q^�'h��^M)j��Jw���F:3��W^����t���8�o`����m���
'�&����ZGt��[�b�m:�xx'�MN��/�����W����!I�A����������
���R;[��{��f sV������OB7I~�����.e��Z�����@���i�(yD�:U��fj\7gusc����v���|�m�7�s��/p����s���j��5aq��'>u
����y�����lM��`M��g���2��U�����!�G4LX����h�[;0�[��'���U�2�
��v�����%��M����������H��!(:���y��*j�w�$��%�j4�:y
l��MS�%*GdR��M(��UlBorYV�zz���j�Aq�/�Cr������R�4�h���u����>tm$lei��qn�X)���;�oz�g�cA���u;��{:n_nm��������\��?'j���%�W_k��+"+)�T�����o�JE�^H��)z�Ue���E]W�Vt���khz<n�)1�WN6�sb�������50gK�����:r��7��$x�v��X�`�s�)5����.�S�df��kF���Y��%�D�vY�.�S��l�x�jJ�g�a�i�UkE��2/;����]
�zh��[�2�u���!�i2�#��_I�v6������m�
�����r,WV����k��~�Kh�~����
db�i��95@X����B�m��,�B�l"R�hX�I�^����Xr��0�icO]���a�I�Ge�B�%xG{��_�1���5�7���}���a�;��QuB�����RnpS�:�
����9��l@��,:Z��Y6�+�+��J�6�3y�	b
Ve�^���7��>����U�Z��q��YQl���Y��T��/%����uv�;{mY]�]�b�����/���-�f�*SI��w,L��;��]gh+�vT��i��������mkJ����e����e��*�~	
�^{o,�f�7�u����;�"�;k��VV�]0|,~\�{���sG94��|JU��F/�u��s����g��>^f�J�c�,���]s�L�yl��+kR���<`�Z�������H;k�J;C��Nm����K��v�ZP�����Ux�q���>�q*����z�d�U�Q��<��U�����Emv���S�������C�������K��:NR�"k# @���'U��"ufk��+����V������I��Y�wc�`=c-����Z�h��U���3Ve
90�t�T����I��*��H��i��=C'��X�N<�J���[mEWs�25�f����0�/w��j8Q������?&nn�d�s�yUB�u9�b���c��:�+-�N�����5M!|uM=���1r<q��V�{7��o�^��f67Y���)=Sd
av���E,���nQ"Hir�������F�6�q����N���KCF���h�p�?nn=�F�7yn��b�(�~q��#<��{����^��[x��S	��2iw��=����UvS��W�raV'3Q��/
��l��w�6-o8z���w������Q�!h�sw�{���N�����v{c&i������o�_�M#�O^���X�����O)�R�?iI_�n��}O!������>���z;���/
�1C&^x�f[�j������:�X�����$�Co�c��{���u9H+~���J�/pY5�������NQf5I�b��D�����4��G���S269+���U��������Ns��(��oif>7f��hHt��{�(2�:���LMonqG53�I`����|e�Gg�m�Tr#�\n�������!v�}��,;���hf�=�{�*�iW�K�U�]���wAm7~�5S
u�3�(��z	����P.^�o.�e�Nz1\{[�4aZ��e�ai�����U<��!C��3l��f������cU��E���iS#2�*����v�Z��-e��b��O�������J3���#�WYYO������'�[���H��'�g�0�����aX�����n��lmb{�V�s����j��=�ow?3�� %�*��h�\Z.9������s�����8Z���L=U���%]�1�wn���	��wu�3�Vd��H�j
�w��a��N��~���7|�mC��:���1�vOOK����oBz�d�O�H��W�`���nYO&BH����o
N�<��f�d�Got�����$�{[�Y���f�r�j{���o�.~s�G�>���r���9V�u�,y�d`�B�s`������TN��3"�tb]��p}Vg^�
��C����_
�~�sP��B����e���
��=g�P����$��9��<���;f�!�W^=f�G���j4���{ ����5~.������q���n�k�n�=R���y���D��>���41�UoM4l����kQjE��S��c���|�9����7��L@}��i���m��f�	�u2�e_�1���[���qwV,�q�x_f��b�[n����&xU;�F�fK�#��S�������L���ju��8Mf�:�}Pa1{Nq�
�6�K��l�������se������Pp
�J��}�n��tB�7sK]��v��d��`�����""���}�����D7^rk���H/����
����mt��3��[���{���:�������%Anu�{
*�6�.������.��rZ���o���Xa���d���
n�W�>�Gd�{|�v���R�
������<�����wz�w�5�Q��Hz�E*�]���T�*�c��k;J���+����~[�����[~Y6bDF��k1�g��������{����1�?e:q��v�-F@+�����E�s-���������d�<H��;w"juOa�7���;4��t�p�Y��"��e��V�>q?�D�x�^���>m��]�~�a9�[hw������r\��^{�:m��n7}%���7�vf�zx�U�9��W�v|r�+�w����	v�U��������S�����f�u�������������V�YF��oG�Y�����Xz���zU�>�V4��fR���&�zik��<���&�zu�|=�+�f�=����L�6�[�4W�c�w�wp���wy�uN���3t���������":�&U�OY��\��7�d;��L��0p�M&+�������������}�*Bn�V���@�����Cu�I�)�
��x���3Z�z�	��
��w����:a��F����v����w�b���HM��� ���.���S��uL��sL>�f� �I�)�[]�.��y����;IO*kXy^Z�,����Q�=��.vg�]��/A�s�V��O�o%�4��|�a�'��U,)S���,6�o/�s�N������-�����2����u��0N�k�~�QE�Sc�?x�C�i�����b�k��%"3];P�x�X}�=�n�B�2���3W��[�=�&=�2�V��:�n�P��jU�i���t�W������{O<z�%���t>6�@Wr�.q���\g�|�fA���%����u�S3���;<*���G������6�5��o=��C��&���Z�)�L�U�;f�������J�������'���X���do�5U7����R������*I5"��obM>��r{O�41E���m��lm�>�T�UP�~]uG�����,�q�Y��t��t.*�Io���y�v������B�!��	om�0��E�����7b�
s�\nH�����]�y2�!H.�w�����{y'��k�&�^�K�h>�����U�����p����Y�<�g>2�Z��;����
W24d�	t
{^}L���O����{}js��q�Ox_�����V�.�2���3H],�
�����T������vM4�T+&���sT�*K�:�Wm��-���6���(e�k=7�&����yx�d3�<x:%������;2T��W\����fMm�F��
!`V���3�f�^��9r�Q������z���x�j���0!��i����O���g�I�b��W�����=����(
;�=a�H���I^������<�V�i����U��sx����'z�E����XPW^J�(��-m��.2�f��'�a�QL0���!6���Qg���������~i�Q
}}�����#s�*�..q��!U�]���d��������W7e6&h��;=��w�+�a��a&�q��k4s�=����$��:��ee�zh�8�e�~��>��$��)��x^���]k��}���l^ng����Q ����{���1�-'��-�{$�G>W,c�3=[o�u���J�p=�;����(����_���������y��H`�=tv�ic;���7���(���A�P�m�.��OC<�Q��4�[WZj�D��]:�]�q��b�,���i�\l��f��7�FN�\of(�LT������$U�]�t����
��I�+}��!��������j;g��u�/��;�"��Wr�lU�H�5G�$��}����n�����`�������2�q��QU�4H��8�������S�����(;���Cr�-��3K8w;�/t���A6�������Op^�Z^
���R��XV~���g���F���G�e�1������z{�:��;;�T+U�S^�O��%�y���t�m&.7�**{��mp��mqB�(h����)��������(d^����W&��T|t���������c�l��w��2,�NB�66b-6"�*���R6S�.�8��o�?
��\���"�Ke�������/a	�L������=p���5u%8B�
nX��n^�*8����+�'V�����=7Y���<��(��yNo\����X��������{|��@��E�b��I=w������Ja\�(���>������R���y#�����|�*�O�����U���3�����g���e���R�����n������,y�	�>�Y���<H���� 6��1���n��(�30���
p����\�������k_��=��L���i��L��������i�a����~���a>�{^@�����`����-N%&.S�[�V���GQ����M����R�oj��	��~'{�����]Z>b��|~}X:>o/�����,��u�[�%zWlf�tJ~���N}���5Ni��3�����,�a����_u$�Y����o�93�6���z���N��X�08jo:{�zo+�C|�)Z�k���;���UBY�C<CG)�n���xi���k;��O�:Oon�C=��%�Wq;`r�i>�Vzn�We�<&����nj����\��
[����W
*�5��AN�N�tdu��1�EJ��j��K�^��U����z0���n>~�z�@Re�
����J� ����m]���(��W)2���ut���
4��ar�}6�W�z�{�mi�M�o�v�z����B��N�;K��[�F�������kv���>���8_��P[���A��:�~���{����3��g!R���H3��Z��T�
u��r��_ws�;B|1�jeh!D�g��u^ ����(��<qwa`��5Z�������WA��]v�z������kJ�HeY���d�&�
�0�\��G����UAfvvJ'\��Z��V�
����,:cc��jF���YF�#3���E�q��%V��;(�j��B��u������e����P���nK�v'����E�h�ye��f{`13u0��sv�JB:8xf�yox��S�0Nx��1)��D���l�4�=|/��M������nTy�X���Z���o��#��$�~/�n������
y��u1:�<+-��B��8�v��Y1���&�0�AvEr�k�6wyR�Y�=��R���{���Qp����/N�-�>9�����������c����<�n����aN�' e,U]�j�Iuz�	�]1���2�|�������h�}S��x��2�\�[��Lq�8����:�$�k��_.��:i:���������O|�j�j%gh��n�;��e�������fI�����wC&���:�+�W/me�-Gs�ub?y�����xuH��lt�1WpO)�V�s�����c}�qC�+k-T��U�/,+~Sv�o0�Tr�����a�f\m��x���}�u��������Y|��b�^%���Qc�0Y^���N�#��I���^S`�wL�oF����)S��o�p�K�rJ��������S����E��b�9�v�R��,���3y��m�O��_I�-�>�7V6'T����ml�t�o/x�/�[/�~c�\�5���p���j5������[o�/��F�G����T��+w:-�����T����:�w&�q9A��h(����-�31���=c4<��g����<��+��u��.��':+3��G�Z���[D�(��8��S������<!`�5��Rv���b���������=[&����W�7x�7�cMd7��YLd�mGm+���6*�Ej�3D��o�\����\#1>��4p��wI�U�W��(R�*Qs���=p-���������w(��y�:�Q���[��1k\��`�*�|��+5�-C�ob�MkF��J������<���q�\�G������'&�`���7+U�A�u=�t�!`���K;�M��������"yL-DW��z�M9L�GX_�F8�������R���:�V�U���bbl��)����wo�gC�)aYU������Nzv�8��3�h��Pz��.��Disd���=�Y�����u�����)��G�o�ty�U��dn��a���v�������������&�e��K�j�g��F���Z�U^�uZ��N��,��*����.�BT!�X{O�����(.�/�����!�~��L�E^[�����uOB�s�z��D�1F���������I��f���M��V�K�v�^G�o��%��[������$x���}�l)U������q;i���	��cq|����fU��_J��2�A���3j�5����|�����^�v�H�N��O6��G1���/�b�K&S�sx���}�����9-�u�j��NK�9+G���r���D?A���_�n���3�����W�����[�a2W�8�MeV�u{s����,'����3�uX����s�:s���</��*������E���A��R��d����v������c���Q�����K������C��G6h��*7x�<�|��u}���B	�VD����;6���jQ�3���&u�rN�]����]R��^����:!��������������B';����Ql3�Q��;�B�)C����:��-Q{[���^�(PF�3�9]���jKB���}�
�T�--L��O30SUz��FV{��{�oV��-Xrs9��|� �#��{m��s&v��'��!Ow��g�4�L=��2�g5�Mu���w�X5Qx}��
a�~j�2�qM�f��{2tQQ���wl���
M��m����7x��Z�v�l&���L���7������^E`��U�3o
��������N�v{j��e�����w��P��+��n��76l�������;34�L�!�O����ZR�U���U?b���N����2����������0�H�k7����pu��\k�=���H���r�V
�kl�57���c	�n��q!g����}��t�����>Z1���E0_��J�������rv&0�������ss\jm$��|s������_i�v@�@K�\B���W*��'=�R������C3����t���`������B�w+�qo'���0,����N������X$]�K[�����c��32�!�5:�N��;�|��b�/�l5��uqE�OcZ���V��J����OGi}���A�"!�1}Z��K^�A:e�K��������aN[���f��4:�y	Y�|iEv�{��M��P�����#p���L�un�M^'TL vF]�V������
sH��"�;�?GK���3� �����Q%T��]�-�)������p�l���i*$,�{�@��K��`;/��yer���FN�:d\d�g]��S�G������Ih��b���@cN9���m�����:����%&����s�f�e��&�\v��K9p��5=ni	XM*��}���6c��j����*��:�sWN9D�7�]��uA��3��Nb���}�����1JT��$
�����6�0��`$4!6���M�m����m��W��g@������m����`��&^
�b�R�xSr�[K�YL�Z.���]����][0n�*������0�m�oj�� 1i���xl�{9>�c�5�{L��>�xT��z���k(fm�Uv$���;�"�R�8�<S��6vKi��������L^a�C	�g�~�J��|�^5*���hS�S�$�/[�6���j����H�DM�-��g}��j����������������h�]����g;D��C��j��gv�����n9�*��H��)p-n`Y�����_.��waSS����%�%�+����q��i�Q
����K_!������	����7��d����p����D���Z~�gM�Tx,���spH,5}��y���hs�o���h�D���/S8�
��Z�s��g%�����S�<�{*�F[�#&��2*�������"�]6�k!����[������y���?)�����N����$��J!�G4,���������7'T��2p��@p��m4%L"��3�8I�fS�H)������q�(�]����<�
/
�,u+��V�:�C���e�<8o�
j#��
����"A���m^���=[��@���%�.�|}c�R���Q�����r��Q�b�1�u���&�6l�Q��1��(�#y��v �Cqp-�2�G��������Svs���N������a���^�^�:�$�m�J�C}�N�d������N$���c,	�^����9^�bgO�ZJ�<~��+'���F�����j
X�4V\=5�xJm��8
=q�Q&X�����Up���6/|������q_$�ob���m��w���Y~����nG��=�{��14)�S�%y(��]�W3K�RU8��N3:2�y_�&CZ]v{R�2�0�q
azg!��Q�9]T�r���������K#z�{�^�����to��jB��<�r���U/n�� 7(����*Yg�%A55�^��%��FC4h|���E�')!����#,L[������4�'/j'�T���ptn�����5j
X���qG1ut\��@�������+:rJ�@cG-4 �_�L���B
��{�����^����|�[=������Qj=�e#lJ:�@��/���^�X��"���Ky��*����r�6o��a��39V���@����w)y�Ew,��#�����'�F�4���f��|T����	��X���^���o7]�$��Js��T��6�L�\&y��^:hHG��m_A�S5��r����c/ck��F��{!gnv����z���K��4o��k��mV�M�r�^;UK���p)�����!/����o����s�!;_��v�
{��(Oa�^lvp�/��B�<W_��eT����.�wS��-��<����%�WQ�6i��yi ������'U�~���yDw1�>~�b��(�U:�UX�K�S~]5���O!e�X8��E����n��Ve�!J����?S�g�;�����q��
}�A���9����,�������m
�y
�V����	X
Z����
���<����aE�R#n[vbG��5W�Q���)�����v��a�t�A�������?n����SkH�����~��"O�Y��k���Jr7x2����hCZS��n���O����3!c�B���T������'����6|��o�����fFY������uK��a�N��!�GZ��<�^���1��x-���%�s$�� ��_oeP4F=9N�$������"�z�:���t�
�t������4�1�E#���������7����(�[�d���OV����u�@fN-)Y$����n������Tf����&�������B��93g ����r�Q�&������k�;,����n����^����3�mJD�6�������MCa^%�[=;���ud���wY,��5s�Y��dU]y�NT���>v4Z�zDB}�M l�\�4'�U#WB;'�4�DU�*��w��\�C1���s�U��&����O
)a�J�g>�on�F�hR��=cI�{�I�����m�9�\�u
��������;Cc��k�M��G�K���$P����}�f,Y��W�1���q���z���(��|]*X;����Ms�i��pK������Z����u�:���n�y�R�����Lw~���]py�ji�J+\h����`^�T�
�������]3�4���}�P�}})��B������Wmz��x�os�79��r���M�q�����'-����mszt���6e 4cc��^sZ�:��_eR��eL*���] o����F��i����X���������v<~^�k�!�l;������^��W�ZC�A�&j��i��u%^���:v�i�0�Z��w �Y7��G^acG�i���(9���c]��S0��NOVz�5f�f�{F���g�*�������^�3����Y3/>C@|����{�^�	��v�����Z�;�K�<��1s+��}���7�\���^'��fuo5u��(��ct����#j�u�S��Z6nvl�G�U$^Pn.����q�<j�&&J�7���[��y6�}Z��4,��o�|w�+3�z��6����-���%#�VT�F���7��Q���6�AO������mfgn����
��n���L�
n\��)Q9A��(!<�ev�<�-�+g3W��'hc
�����g:�����T�k�rm�S�^�����F\���jcb���+y�Z�i�%f�%���n:�;�.�z����38c`x��AK��JH�`�O����<����:B�EE0�����9��m�;9��c��|��P)�5�3�����|g���X��(���J'u�	�9���"&p�=����k�]�i��d���-+�e����{�/K������ob��('~�5���]R�@wQyY���P�v�����t�����n���'������zV^���Q�g���3Q����5�z�K���:�����i��s���b��
>C�u��k�er�������^RuT�"��������pQ=��OE���!�s�_ud�b�w�.��&�[��:oX�W���]f�����O9>�#_w\�"���:)M��#�oT����|�������&�Cu�x���F3{vC)���hgu�#j�5�����.���Y~��Q.���f��W�T]�������U��|d�}�'B�M
~D�Trt]�,n�M�8����G��~{���U�������
��/`�u3�p�q�2�������2��0$5���1�E����;�,�C���Hz��I���	�=�y�O{+n�9���0������^/A;�����\��i��$��M.���NK�"�X��.�5�^���1����v�����=M�h���8�}	��J�e�<��]�]���+����7*�{S/pV���Z�W����N����_)�����]��s�������]��vk}��e�<c�������=��Y�t�8�'�;����[���+�o�2&3�<���(�J���o}�jqO	�e����q[��u�(E����dC26�rz���73��b42d6t�h>������#���.(��Non��^���1=0�Po�H�'����M5�������%:oTh�������tN��������c�.M�I�M�8���4�[��/c�&;���im;9�FD�J��hJ����7����������z�iY��K�4mA:���2�`><�����-;S�SLaO6��<j���{����\{��+{�eX��v�����������^�e�v���J�^|�o�]�[s�#��Z�����r�1�igZ!���rRc����y3���h;&M4*�
5���S���J
���d�6yg-�cJx��U���]<�O���i%�6U.�R��-i�� ��|N�4k��Y���
.������m���9P���<76��_���cLdU��Gz����~��a��0^����2��~���,{�*��B,Ek�s��{g�0]K���Z�d�}���U��d9`P��7�S���:���d<�+�dT��n��O��6��x�;,�������@�n������K�L���C�n)u�c�aN�o*f^Vc�����������R�{WYO]��R�OA��e������;�}��g0�Z������0���+�'<�T��C>�n��N��Jo��$�:M*�� ��x��;�����r������9JLk��z����j�2��nz���r&�}%�jg��K:{��S�%�iF�$vS����Y���e#bL=<o^6�++Wt���R�&�]����
��ed�ya�72��{
o���v%^�)�������s/T+�%��S]�������s)l��A���������nP�G����o\�%��\������=y��IL#�=���{���~E��zR`�n�b��w�q�v"�������+*��q9�����'S9y6�PN���F`���)sZ�n��&���o=�j�i�
�w���9����N������s.�?yIF_b�[};b�db�;��wT9�����U�7��Fk��Z�7V�M%q�Y��V������D��H��������K��Ip/l��2�}���y'�]�e���*U��#t��;��jDt�yF�������7e��N�S	 �^�[��	��p�� z��C8�[��h����i��n!�'}BP�i5��*���}����@�x��7�Qc����*���E���y;�H^��K��Tx���ewE^�b�fAXe&�W2�-���uv v��v�K����c�"&�7(=Y���B����5FufL]�������U�[yu^��(�9��#�%x:�iJ*��������K�M/N��j6�6��|e�dq	�/v�KW�B�Yf����J���\�1Yw3F�%��:9<����x�a%]nv� �V��1-����/���_A�We���.��+����e;�)g�|2m3�dy�'r���zHk���zQ��CfK�>i:�~���L�^j����ky��vb���}���;������&�����h�,�����f����Ju�����yE�������%������������G*��V"uE�+6�F������$��1���{��������m������gfI,g}�.P�pU��dvF������i���2/}.��N��Hd�����[����R??���N�����J�	7�4���yY��h2��,	�j��h���Q�B@��3'��e��Eb�6��P"�3[o���G�Q���oi���o��i'���>t%�E{=���:v/w�
�m����c.�4fi������F:X��wWWdK����v�z�I���vX�\9%�8pY��9�����R����������/��}�U���Ay,��!�����cX�|������8���+����ucV23�n-�����B�o�>Dm>�|���~}��jC����<SP9mL�p�+�V�p�-gj������'6>5�e]]d�%z=H�QK��+Y'1�g+P5�r�])�&km�s�������
� ��6��5�n3I�K�����S1L���6�P�u�>+�u���!��w��2��v;%����*��Tb�f�e�q���vH���f���}J���^R�z:tb�z�on����~��;f�����������{(��=]}�����X������ED�Tv
*U���7��#"����\1��z�sV6�����,���`e���ZLfN�zvX���!��j]�+������������J"{S>X_���u�d��U���k��[S$��.�����[��`�D����WS��K�������vsx2���7�� ����*��Y�B�,�x�bi�\,�Z�L��:'�Wq{�6�:�:����;.���}��V����v��N���i
�)�:��m\�L(�����B�*/]�o8��������}�\��c�s:����V�v�Y�J�P�$��OX��V���@�b�:����g�����=p�=Q���{�C���t0}��j
��u��^u��F��O����YM������r��]9���t����K���c�,U�A���}�#I�=y�.�����<��S�vZL;�*�k���q5O`�����w0��I���Y�du��K.k�x��g�A���&���L���)���m�73pcF�wS���&U������;�c��nz�_O����=�=�J��dW
�L����[2)5Sf�	����|����S�N�=�#�um_:�67v���j��	1xi���;9�Vx�Ty���;>0�����S����W2�����n��%p����>Y[��������@����7H����M�Cq����W�D����L�.���oj0L������HRO=��'�+�v����c���	��y}]�X�}gw@Z��Wo���a0�Ac��|�%
���L��P1���Lm�cb��[W���}����x��
�K������R�����RV�tq�w��:m�������}P�0�����{������Y��F���1����=r��6P��l�pg=�\k$^��^����tD�,|8jTuB�U����K|���L;�=���b,��4]f�q;K*��Oe?}�q�"l�V[�BSU=Y�u,�,rP�CN��i��5gqd)*�}s{����e���JZw��]��ek�)-iA�����f��z�]��[(�M*����
U�W����w>��A�1F�b��Q���S�t1���\�dnM+���p6�2s<�B��`���)_\/�vsX�f��������m���3i����p�8�<���z�D�����f0��qr����x��&�o9*�����s�O����&\�h�����\�)'tEW)�&�3������(����<���+�DB���O���o�9����Iz'��]o���1S��w�nX�,��x{j���/}����#�����Z�;�"�U��i�=v���������]�@4��>��)�7�����!^.�����'�n�s��@u�6Y��c�}�C�_q�<3�z*�y"e.�#���u��7��X[:����|e�-���{6
A{���~�e��;���H��4W_��m����"��gNA�.�aJ�N�Y��DT��ZV���\��KDI��W������3s���L��
�g�5A�FfF��V��(\	Y�K�� ��X^��4{������V�����CQ���-��YBR�O�(�`��op��`y�|)���?\U�gb�!��74��<WU���o�k�V����a��pnj��K�i��n�f�L����}�m���� @d�����5����z5P��/JC;���������z�Dz�Y��v�K=�5c}��PmL7���+�:7��_ syN�[��8�HW�n_��)3D�<���+��#py����<KZ�p��[O^9Y���{�];��Oxk�ds��P���������E	H
���8���Q�,�'n��N�G���am� ��r1o��=[����r4�C�{b�;R���r���>�{&p��B����K#��g�c���.����UY����1����[V�j���%��8m� /]p���� �Q�-���<i���B���OH��v1���=t�_C����Y������D��B��x�
iw\�,CU�pk�JLoA*B���z<�\�������j����x�����C���?�%�F�*���t�A�{��c7����X�������n��{������u�+or�W�������������F�Z:���<l��zFo��Y�Q���4t_�*i����NU�N�.���C����/4<� 5\���jOf��h�(���~YYH�V���G��2b���s�tK�h1��!�i�=|=on7^0n����Y�^�oMo���^=�{E��������}���#�b��'w�G�P�/dK����r[U@���.�A��@���o
�����V[Eo��s�&o
��=FD���-i�S��Mi�����N����\g%���C��W�"��T^����H|���7S6��
�~���J��u����d�"���D�v���1b�����s��9;0��]�.��(��������UE�����n����N����G��{M�$A��;�3w���:��`�n��t����9��L�F�R�j�T���.�M}u�s7C%=��&.Uu��3�Lu��F���l9.M�<Bj+
-���)f�������^�4�H�k9�L���YU$G�2.|�F�:�*�����$�^�[��LL�����d����[{�4���1�$V��\���]�H�ayYfo���}���=�-��'=��v�vz��ko�>����{�����SVD5Q�vd1QhE����b���g�f��T��7V7��2����J�������'�-Z�f�J�qP���J���f+�SR�o&�]��=B�G;J6����U��M;�n�����v���z�T�9RU�R{tH7�7^�^*�"R
sT;}��
��fm��ED<�)��G���\\���}4����@�X������m	�Uo3n�YK����4�����m�J�����>�7�f��Xp;��=4�z���iE��;�N��f��!��������c%���+[N���'�O.�cdY�c�5��N���O-P��dr���zq�Vf�lm&�L�aW�
v�f�D���T��6F�k�n�Ezn���gu�<r*��x�w���+��������S�1�dM���2������J9=��W�`�#T.���v���i��-���;����O��e3�H���n'��
v!Cm��A����6���w<uy����U%�{��������iy��,�w��2�6�P����*`=	�
	��K�TU���%{��/��=�	Y::�Y������:u�2k�#�����K�Q:�����GF����N���n���!����Iz�fn��+��d=������8��9���5{7���7���k�xR���v����,o�3��~�s���hs�J���Hi��I���K�*����sj�
"X~^�t��+�u;������������-	��:�o3n���=;��j0��B���z����.r��N7q��r{C'�c6{�TuE�V��r��������~krL0���IScd��nq��iy:��X�MF�U��Y���gfeIM4$���
i���%im�7��o�s�\��~>�
{� m�����P{���+��2���:9��y.��|Nf�ZA��\*����-���62����x�{;�&�����.�.1�kuk�M�N]���#H�K:����Q����RO-�bsE�
�ae\Mt�w�3
x������lg��������B6�NS�jN8�%xw�u���^fv�������T����B���F�:��4�Y.�W��\+uZ���$��]P,=�z���>��O�B�3���=���r��^j��;�1��ym�N�}����jk}F9^�
:qy�X:�P���A�r�-���M�����S(�����;��m����]q-<�~��+�z
����3H��R�y);Ju����9���}��Tl���_�u;{Fc��������|�8�6M
w��n��\�����01����K
k7�'����g��u�iVL3=�
V3������s/�W����J������9��������%�����������.z�\�b3���;��.�������\R4���mq/6L ;�����}���p�EOn��c�p*p�)�uY�o�����E��;�z�P5f8X��T� ��B�n�n��[JsOd��G640�=
/{j���;<f����w�o���)�d��!��r�~�~�O����]V��e�
�GWA��E����Yz6���;�X�_a��{xl9{��o��M:Y��!�sa�\��7�VY���Tv�����I�WR�d��I[<���g����:�����F���_���;�
���c�1��ngA���R�43��:����x�.
����o��fc�U���+'��L�V2c{�s�P%
�1u4|���!�^�]9X20��^sH�VT��!xL�;w�J0����X��y�=w+=��VMEF��g����[�������cwG�EW�s}z�w����l_���=)��q�{�������h����#o�B�R��N��v�	��"�K���y�����6�gi�l��mF��9�\X>~��-M<����;�����+2��2�)~9N��9�(a��������&l������8����ZU�3(o<�?l~�''`�m�]}[�S��q�Oj���P>����d{�K�.��+���L�7EI��qx��K�t�82�LmV�B�����a��5,��~5��}��f;����u������%��C�c[Gx����u�%^��a��d���r:��'�A���orm��.N��q$��6���]N���>4���
eGD���O��%BG����nlip�r�LL�������\��.�zX�!�,LR)��f]f?w��y�!gq���7��.�b�f������c�z�(��
��wv �z�h��b��\�,x�o�\V�,k�,<]��:x&d�O-��!x<|�_�%92����DZkq�B�2y+�nfZ�S�\��{Q��5�n����-ci�tz6�����3R[�v�d��u�M�R���(_���~^���.���8�
;��
k�O%@����oM��*wf����Pb�_Q�o�dh����{7�<���A���Guh'��yW%�
3���� �����n�_.�����I�L����z����P���V����a%��� �Z�g6���N��t!�K�{���^���x����i�
yyqY����V��&��
�������������b2�3}�P)�.;^D���ux�
���I��w���R����_h�P�aP+��TO���Bj�<e�<qUI��=8����G;T��]������?n�&��o��4o9�}a��.HK/R���5)�v�U���'�C�tx����2�*��^������"�����5��=��
w�\���7�Vr/����y���2�?(.hB����J\�&H�kt.�W�c�z����v�����]�9����9�������o��p����n�����b'�B�]S��i�z��N����c���S��F�\�(���\����W�z�*���GB�������w�gX$�\��
c��t�>�c�wL����XWN��*�0����goeqeQn����E�_��c���.��/=�6�+�Cb�I��}��+9����Z���n�)^�sc��dJ�&���e%�W���k���+72�-�����ig_�a�>����S�������]S8�������krqT��R���K7��z���g@��=�b�1B����� -����(�+12��v�2��}t�fjg�`bO������+O>��%�#����_����j��6-��,���[�� �al�����A1��7�������z��qe�!���(OhDuuF��U��������#��v�i�re)���O�{_�������������"�5{�^�
q�]��qr�Gf�iw���������4L��r�e���
$��~!J����nM��N�R��0���C�Vf����E�{g
S���Q�k�|F��Y�PT�@�r��qv
�Y�6p�EJHyu]���fq�)�Ys{h�e7U&^�K�
�=����&���� �_�C_��X9<�UrI-�o$�8������{2{X{|+�M$m���g)-�������r�2`�LRsq�'<���������q����&��.��FZ{c����1>������5=<�Z���
	��'�ws{����o*��U�k�������f]e���ci��y��K��Y^�_�����q����8s2Z��qee���^z�3K����Z�Y�k�O}��n��[��UnP�'J�Y��u��6���v���oU�<��S"�j7���H��x/S�WQ�v$[��/k���0�zS7s	��`3�	_���AV�E!wi��������5_�����I[7r�7�L���������z!�y����j����/&��I�R4�\=�j���S�C�x�"���nG(��F��>�v:@f��8��2_T�9���6m�������iK{��kw
���
 ��8��}�9�)�7m�u�Cw���W�t0t�E���Pj�Gdw�C���A��
e^9��������1-�p%u�f�������{���S������y��.Q���Jw�K�����.+�������yu�f�S��r���~A��1Aow��`��w�D$�P�*:r���]�';�V��$�+��0���o�z	[��IwrLC�j�}������*�@�O��kqJ8���E
5,=�{Wr��N���5F9�0wU��<�{��o;F
��efGg:�����������(;-�������U�U*�)<8��)��Vq��L����*�+p��b^r=Y\+�M���]��n�%R�T����Vw&��8�x���y�,f=CYG���:�:P����m#]��_U��:����q�D��g��a��U��3��x0�qz�Y�7!{�^ �>��\&W������m�6��B�����WE��2F�E.���w��$�����m�pj��6�{���["S�����
z�*c �V���J����U��[E���J�w� pgE'��jW2$��vs6����=�5�@�O{a&n����������.�S�gIh{��9v� �h��b��-��1��X���^�:g5��������#^����������>�5�:��r���Rz��O����Q�{��.Q.�3v�����;���b��8�u����z���L'�3�TK<���.mU%��8�&=������sz+7����<���*v�nX��(v�����O�2=0c��1AA"�q�9�I]Fo��r-�{�{k�,�1U�3�����-��~?s	���0�qk�����f�p�:�[�9��*�R�a�it�C��OU��"'"�;�����m�"eq8�f�L����z����f�J���-�2��g�z�������B������Fc�b�"�E=���t�I�j�iH]���/Pk0������}��lbb�c�j�)��Rz!���zR�*��Yt�Z�b���@���;8��|��6c���iG�a���+�Q��o
9�x����=~"�l��Z�������A���{(����,��+uv�hev�$��#�q�@^��/H�S'j/3=:9wi����@|zRY�����f]yq�q�����|p���*��tc&�Y��Ov���F�e�������9�x���TwiC���X��������0|S5�Q���0�w�~9����-��'zz�FGf����c����a�<�\��64���1+f��������t���%v�B�#{&��;�YN�	Sfw��Vdqj:����
�*�9o����x��5�l��.6wEcf�{H�g�����O%����e���C\}�������\��J�(��u�mB^8]H7f���zrB��9*����������d���a����S���I���N�����v1m\����F�����E��N�����x��S���nh?2"t���������%HlfS�u3s��YmE�Qr���(��f�;L�]�1�h�Bo?�����p���(�������J.l5���y���|�B��g���o"��O���n����������r�T1�M�m�x}����i�t5�7�����sLU>Y��7:
*�����3���1o���.�Z�u}���Gv��NE����n�wl}�w��ET�2���eNi�����R����L����5O8�U������B��f�.Pw��������p�����%����X��xV��we
���i�+�
8�h��\�e�����M�z�z�5���v�y7�!�������[����p�jjJ��!,OZ)���j��UV������z,��m�m2R�@�.����BV��h���������G&?]C%�j�����"��r�7+4]����^f�hP`B���R��4i�^j��,'V=o�&���kF�r���E�s������%oi�!�`��I�
�
K�����/t�������)��t����,�c�bY�W>�V��of��������w�j+�]�����Bx�{a�b����'��)`�����q�{/9,����8U;�\-���]�)�>�Q�P��V�9SQ�����	sLZ����z��%�UwTU���8w9�s����b����62)�5�����=v����6j=���tS���,�
W�w�+D�����g��9~��Sc91�9�'3�
R�%E���������j.�rj�J�0V���b�:E���q�:�/����Y�O|���N��_O�C���p��yh~������.��J�f��]Z�m���`���������^�m�n&����d������/�Vy#�-c��p��`�Q�d��\����G���kK����{[:���*mi������<s�P�f����n�o��%��ks��eyxsgr����(�����!s�;���*�~��wf�� _4'�^ZpWT��r���|�j���N�U�}��Q0��{V�������#v�ssJ����GZ����k)�K3��:��f�|{��8�S0��@Y��<�:�����Yb���Wq�M�z������|������e2�w�W�c4E��]�s�)��xs�bT�������L����������x/jDMXn�f��0;���0+��g�EuYRc��E������9WD��8����	��;����n�NIy��_B����<��v)"8�6'n�xt^}l^����^E/�-��Sn�6���t�P�'.�3&�
�B������_���w7�bj�,����w'��[��x��
&-�}\�K�^o0���_vk�f�4el�#�58#�q��S���-�k��"�>���T�gH���F�./j���H���5�^�O]��������i�Hk�	��(�]Dp�d��#Jz�,�go�����H��ON�������i���y5��A��FS�K��2�[�^���,y<��53����X�`���U0�J�+U�������=W��a�j{��EZ�R���z�fa[�j+��-T:f:����EX�n�~3cY��e�B�T�~���]�Ok :}
��(���N����Dl��/�����S��S�=�]�b[�vM`r[8�u�%m??�lt��p�4�xF�f^n���_ds��y�I���9�A�j��C�� x�����w���s-�8�P�rg��C*��,<��q��a�*z��Cb�L�Alv���m��G o
�e_�P�gsa����v�H���Z�e8����f���_=�m!K�Y9�5���f�rJ�n�������3� ;=�^�-^����}g����oc
����r��.&�,9����b�T��=��k�n7��1���>�������8�{�K�y��b��&u�0���0f�;t�T��.�L��`�	^�1w����
�i;���	������5�sY��To�[
^�!S����T��m����Y]N�����������y�{�����_�f�%<�y(���`��{Gm�j�
[.�y71�����u�����LF<�c���'_Z��`T9�r��-��B�j��,Lr#j����<v�!��o\)F�z�����������
���v�T�Z��[���\�JR� ��]�����}��y�����K���"��=�����\������]����H0*�����-��K��1Q�����pkR�%�����=��5	����u��5P����h7n��u��d'��o��:���k�4��b��V�^yK��mR�����OCm�}e�{Q|��g�Eii����r_^���S��p
w:O�z-%�mXz.�Pa�����;�g��Vv,�>+jT6F���������GK3�XT��9.� �#�W��S�&��~nhe�������w��^����gS=S7�t���}������lw�#K�1vs�1������g��	�q��w�����b[���7�����A,���^3��p��Yh1~�jq���V�����A8�'����kp�����w��1F8�	���7�s�=K
tq^�\�����]M��4-:/��K�d�:���Mr����Y���n�������v���2-� ������4c>�Z�����S��MQ����B*��//E���s�.V�7���Fmh�+�G��g.��o�I��OM�=["�"�q��t��;e�9+(�]]�Nw@5����sI[��(�xF��������4���yw�~�T�%�B�c8���"I|3�u�9����b������������^
�=�9���7>�n	��V�a�����'�*�2���WQ������{$���0��vv�YP2�(����Yjs�yYv}�!��wa�L��F�Xp+����������U���"z��*P���&�2�8�
��v�u7v�z�j�n�dJsK
�cx8�
��R�4yJj:+.�u�:c�#x����n����������p������`�v�ug�u�V�0��X9M���I�=����3^�������NR���ShQ��%^��9Rf@�=�s!K�..`�����E�������;O�t��!-��R��
m�q-�@�����]M�q�]��b�s[����+��)vv��*��D#1t���w�m�:����0t<�c"B�t�2h��<\�g:{�������`P{W�y�z������!�]>�h��r[����or�yct����JW|�U��;�Z#Z����U5�v��r�yo7�]��.}��O`�mE*+f�f�mZ�u��-���7u����pR����fH��~�c�Fn����'������&������[�w
4'����9�����;��r�5���R���c�1�^gtd��z=yQ'8��=�ug������/bb��������~�l�i�P��������k6Nk�M�t}�@��c*�������$����7.%�gL�e�]���,�<���k�bj������)��+�ko�o s;q�%���s�����6%L�@��R;.��q�T��|U�[�c�fO���SJ���x-g?D�����;Tt��y����4���;��5\!�"�WtS��v��k���>�<=�F�PY��x%��)M*U[
�lv�nP\��I	���B�����W`���w���:{t;�\k�N�M��c��:����Sn`�+^O�i!N��V�3���w��ie��
��+����d�`jn��T������c1���.vW'1�AD��u�r�=��q[����N)p���5H����n�G7�2��N��J-�O+��V=�u[���C�u���7��Ln�o��20An�[��f��G��g��G@\9����q����v�����F��^�e%�se���0��f�
�Kl��������]��i�e�"���
��������~�W���S"�W������*��]��].���r�*t�-��b�3xjK�
��]I������������{�9�k�������f`�b�c��F������o��2�{��u����:�f����;����V+��C-R��Jn9N��r�M)q �����F�nUu�����Ch��k�3��E�����U�^����������y3
��J�������
.��C��m!H-h�eL�i��7s���XV���r.]��9^��7Y�bJ^>q.7\;��Y���F�{�45�����&�����T�}V]����9�g��C�P��qtQ_�]�������]���yH�]������������j��<2)K�D�gU�J�����uw��%�Wg�����i� ��+����G����P���V3C�.��Z~�X����v��M2q�N�k8	������W���������)���=	��o/����~��m�(U}�$pmu�_z����ddd�8��tem��M/_�\k(��l�q��j�;}Jm�������&��L����[O
�\z���,*��Z�4vZ����3�e��Z����c�.���,^p��sy��F�[^o����T]����U��=�c��^PX��W��U��2!}�ci�[��	����[�k���� ���,2�����B���p�;
`�����2���z��K����R������
%W�r9|.����V��������E9i�0kqW>�X���8��m��uv%�91R��Y@b��e�o�|6zz��B�����eG�
�s"��>��~�=,L>��������B�����h���J=������X�y���];�@?w%��eyc���f�v	El����\��tJ�y71Xd����.�����v��l��=9����m�	FtY�i��R�F��
���c���Tn��y��^<���q�Ky�T�Y�^f���Ekj�fr�e[���}��N�w�5�G)m�����-P�t���oKO�
�A�yd]���4�������gbu�w�i�]�X�u���2*�zk�������V���x�G�G���{����M����R�M{L}�e����]�
;O5�`-vP7-�����
w%���P����!�M��96�V���L���n���-�S/E����pM�b�u��2���3:�G��v��1�X�J����[8<0�W=��UE+�����v���z�WGti$���'f��m�P���h���wRU���NYYg�n��������x��L3�H7��SP����L,�1���l���\��V"���m�!�y���H1�m,���2�iO:��6.b���
7b�H��:�X�P#+�l^+��.�H:�>��8��'c;�T����N��q�:x���;��"L�S��Z�b�i=r���V7�����"���'�='�v!�4���U'z��Cx�eE���4o��V�U�`e�9��E��e���M \��
�j�6���u\����b����<HRec�|
����c��I�;�:���sL�*=3)eJ�z�����r�������9�=(Qo���)%��X�[.��V<�v����)Q�
��e����w�8�S�I>���S�3
��2��\s�Q�!��(Z��@)�}H�!��-]6�D�;c�m���@�-�e��t �V
����D���b	|��6�[Zr��y5�dx����D68
�o
��v�o�>V�t���@��H�������� U?1���������q�W=���`C
G�:y����/O)lH%{���-�>4�K��z 
�w\��p��
c��w������V���(��8Y]\��q�����V<[B�l8�-�g��EZV�"�u$F�����=�6*�AuC��:�P��M��o=5����bF������.-Q�x�S�7���{��_?no�� ��1a}fa�����D���[v����d�����3��a��i�����(�<=�k���a��?IH�A�J����Lij���#��]9����b����
�����`��tn���v�m��!.=9�&�[Y�7U����4�,�hfiAD��������1_^�l�r�
��������q���5�c��>�����w�H�C��1M�v�C��������Al_m������9{'�������'��.�{9�=��S�v�+�5'@�z�����w�^'�7P��+��x3j���x�����1�6l���
��,X��t�(@hC���7xN�h�����P��s�z�����!9��<au+�|C���N�N��2�?tY�[b��6�G���v^�{=��6�2�gwt.�1[yp�+�����]�s���Z�Y0�S�	���:�F���o��37����l)c3U��J�7*����v7��@�w���[�;�-����.X��[��g.�b�S�2r�����
�����2�c������'���:.� ��&�M#=��~���Y�2������+��������.[����Di�Uqz�����;Ji=��@\���H7�����-����:�re�}��+3i�QP���tN:Y'&D�z�Y|t���}&_l���{X9����1q2�[��(+neLnR��/������T�S������+��|��,7��������f�'�Y�k����~��	d���CTt#4���+�,������x�1	���V���f^��"`�b��`�)�N���Z�s�V�&"�~�����]�3�0��B�Y��������J�V���Z"�.&�iy�<��2,���au���:������S���R��u^�tc\���hj��;x�^�:���q�������%����|��s�|�d��2�0� ��;{���v�V�+v'|KS��F��Z'G^` ���R�r{ ��a���(�iq�������a�G��Dt6z���=�{p��)u��
Zz�_
X�V@V���*��xY�:���z��}��~��*�=��;��2�g��`�!�=F��\���Z����G�[4�����`c����k�K'q���N�*�������=�s2���B�?d��m�%F���eD����������Bo%��,����3�����o�\b�UU%k,������+�*{�v�z�#QyW1L����=w���>!T�;d'.�F&-�V�Y�����c�IJ4�9�^�;$�(]�"U1���f{W����z��:;�������]/s���+���+*,���v�i���P�u�UG�j�Fd�rU`���j��W��$�rP��Y��|������_`k��74��;T,�[B���.���n��t���u
+�����).���X�g�lA�%u�|�7�������-_�Z������u��s!�M��������{u�XK<w\w\p@
�`�U�\����Y	c�$��&?����O���Mqm<�`�Ju���X'[�h����3V�{����x��Z�l�[)�F���^�`�u�xN^�L.������f�H�&b����P{�V�K����y��Q��:�8
VZGvo�������U�b�x8�����v���^��^r���N�j;G�
6�����]f�r�-���&N]�����L�\[�~���xx��O�����]N�h�hm����������qtP��m��'���������WQ��x����	
s�:)C����k�������P������n�\�o~��o�����>�xM�T]����NY*��K��=�i����M����
A��{��AT�\c�8�����j{��~��)�Y��g����A���f����Q�w���;��v�$�@����zl�zk�j����Ow��=��a8W��7/��A �Vo���;KXse���z�����^�SoEd�8��sZ�]�mu���'l_8v���:�8��T^��_
�54�+^w�����7y���7��F��U�`����ohwY��"�3*^�}(�}r�����9K����pH�^v��/m~���y1�:[�}�Eu	��]Y=ub�����w��@�.��Q���A*B"�O:��]�E���}�F{��m�O>D���1���|���G����b-C�?dT.�o�b
O{����O;e[���Uig7+"Q�����k��^3bD{:��tN��� ���$�`�}b��d��
l�vn���w�����5���������|����S���������[���J1������������e�jwu{�m��:l_�s��%9��J���E1X�}x2GS�1��R]����f���������
����R�{~Z{�������O���rs����@wp(#�3}x�����8s{
�e�m�c���a��u�r�������0�Y�y�uaGK>�SsR4r�������H��K��^e��YEv;��:�>J�L�k`�m���3���#B,��r�]s�dLk�
��Z�����>W���);r��Q���F(���5����C���c:��S�L	��0�������B�-A�za����MtI�U��:�������Wa�^�lzh�Wm���T����gy����$��N����
���n �jg�����t����Gfv�Us�����t��������KR.%�����f�������;��\����WlR��O�LN��)6+jCq<OD�{����7��g�
���K�u�[���������#�L�>t���xnVK�!�U�3H�f���T��+��m]�W5��������1V6�r��w[��V��i��S��z��O�[��EzJj�v�T�y��(�Be������������R������=i�2o2�+ts���j����|�$�o�m��v7��X�.���g�Gb���vm�����g�(����z�&7�7��F����"�L7k/� ���3���9p}�k��x"���z�M6{����|r��gPz����ow�k����KJ����w���	�w�3�W���W�TR,>��N�EY�.fd���~ou��hv|,%m
g]_������k���`yKV>�����3�r�����%��1����B��������6������w^�r�=
H^bXGN�����O�i��j����/g���X���v����:��Uz�34[|]�u��H���9b�������{(9b���Ca�<�
E^��N�z��v���)fmbt����7e_��^�u�E�
Dwm��6��u�3k�]�S�Fa���,�j���(����\t������<e���x*��^D�I�p��"����b����)pN�B���/p���|p���T������4I�4��u�;�4���g<S{�]���Z����FOn���A��^�d�~�2�]l�/;��5��)�j��%��^�]���s��}�-*��K������myb�	[���lXp��w�����CMG(lN:����^�	�X�E<9UV����@8������fp��&V�V���K�8S�u;B���Wic^�toT����K�������
��-^�y%d��A��W�s52R��W8y�W��~G�����\��^!j�b_#��sqgvxf��� ��������+�����A�(K"G/�z$N���Y���p|u��b���78�����a�t�O"W�t�,;����|�.yF��%1�m��U]���+Uoqr_����.�NvC�=�������{����C���I��<�	�����J����f`�L��ka-���J��2�}�FUd�{�W��n�gsw{Be�����
{'x�E����S+�I��j�%��#�N�'�\Uv��t_]vL���Z�|�q������u-��
�7Q����UP�V�����+��m��9�'I��=��&X���=Z���[�a:�,�(]g%�G�����~C2�i^7���9����O�V�������XQg{y�J��U�Vy��k;M�z�����~�Kp@��>s|��*mL�:������mQxh^��~;��k�S����{�Q�[}^�t�������mx���6����co\�q�}TM�����-���[���8gF�������=���������t$��T���CP/(II�>���kI�U��2>`
yi
��I�����t�z����k��M�Xw��%����]rC�#P�1�21k�{�	��G�r���%���{������|�,6"{\So6D&rQ�����Y���h_9����$����\�~���;*E
�c����y��|��	5�Y0��]Zw��&��Ne�������>�����#*iA��!��y<�Ku�M�Z*�B��b����]�tQ�*�4���ux��]��s�l�����"$b0��5>3"��H�tp�0{	�����5�"�Pp�hj��u���\\�	
���"��+�8
%��,'�y6���4u=8��W�����{'�g>�7i�g�}���CBo��L�����h:"^�����6���}�u3�gfzM�'?���t��i	��G���t^jZ���W�`W[��]3�/E�8 x�y��������m���N\J��|5���z�������x�1?���OZ��AdMk��^OY�:���u-'F�JN�\�\;��m�Z��/i�N�/�
��������Y.� !5x%�\4f�������Aw��m6���\5��y��0]*RS�!����^/h4b��Xq8x7Y�mb�ag���~+E����:qa�
���4��<����1�;u���������H*��6i���3r��B�;�v�y���79��]���^���3/�J�oM�8
Ul����'fn��3��|��r97� ���>�dS�0���
���^���U�-%��J	�y��'N����J�~fa5s3�/Y+��y�vw������p
Q�,u�n0������u+a��3=���dnJ��\��n����� �B���|��d��V���=��,<�\���d��*^Hs�TL>YfVC�+2�Pr3]���{�q�|���:���=$w��*�����n�N�n
�I4��T��������2��H�@�+�����$�h,.�[�-^�Y1��tH�c�c|=+���5���e�uf� �^Bn�������5����U��pE�:+�7���e���u!]��o�o1��������Csv��9����/
3{�����[�HpuR�Ln�v�+
�z'Yc�w����o{
_L0�	S��W�ch�����M>���p�q�Y�a�z���*�R�1iOh}��.��#��r�����M}�.�y1������YU�E|�V8mW=���:w_SY����}�:7*���t�����Cwg*�� q�P�������K&O��y:��y-��9i��� ���)EP�Z������z��_t���	j'�����Q���>Y�����9]X�t����2�;���yW�U$��8+��X�!��|�+5n��u���D�R���W�E*z��KO���E������oM`�7��Q����)�wm�;1��O��y�rt�q���U�V����:w���6�����JMX����(t�z�ms=�S��Un��{;#���*�=�3f=�����B9Q��D�:���1�;n���|$���s�Zd�Y�f�s��
���b:�]��Zy<Mx�Bmy���Q/�k��Uv��Tt9
���N[��f%j�g��;*%�W��}z]���y�Gg_^Zk#J�rI���a��D�%���z�"�*�����b��[x�Il��7������{�*a�����bg4�z�������P��@��jZ�t��Y���{��p�U*����5�lm^��$��_�,[�����\mr��)����h<4���9�IY�M�1�@�Nd�$R��xrH{d&��;� [Cn�������~E��N����x�������X�d�r9aQ�;����|�����] 9 ����f��
6By2yJ:���q���������������x��pW��W��
vER��	�KW�^��9V���.���u��w�7���������lR�/�5n����"Yy,Q����`���J�"�����]�d�c^��yu�a&���.3"�s�&�����82^�)�I���)������e\	�ejm��HC��`������B�`�(-���{A��X���-����c�P�E�Wq��e�J�F�e{�+5���d���*`���.,�@
�3�!�t��Wp�����Y�����	���u�$qk��_x��}��P����������W�7<��`�l����3�Z��^��V�]�_)�}���5���E�U�'�En�;�%(wX[�.ua��l'r��>��8:�������v�C�ZD����\S]���=�b[��t=(y�5�J�E��f_Ei^�|����Ze:���\���d-���u��O/`0:[Co2�T���	�",����a=]OLd�UH���[4��������]�2(��GhS�\#���9�����P\�������7�5fAms�%O!��n�r���f�r}C`����qp�zT����zY�8�]����K[H5��H��Y)��y���F{�
r��cQ�����O��8.�
��<]������0����Pmv���%���I�u|�,�B�^�y��qk��{\�M������WEd���*������EX�9������v.���������H���[�gw>��n5��^
��x�49s|��H��h��r\U�q�#��fS/��>��^L%r�n�����@�5�O�����+�2%)y]d������cf�wm(�Y�6�_)�a����{��G�(�&���n$��t.�(X�f	���r�#��O� a��3���O��3�b�K��c��lK�����!��og�X�wY�������*]a��N�A��C5��{[)�8���+.����j�*�L]���^gp�H'��R�jcg�j�u��x�[d�7�N��'�4H�R`p�6PU����O������-��s�����B������u�t>�[��&�8��/�%��m��2�r�
���zy���.>����=3�P���Q�}�]�����1)Xw�P��Ze����
[R��a�9�LoM��<����H%C)eGF�u+e��t�v�g%�&:�Z�8@�cS���q�����Q�G-�{O�bG>��wZ���+�o$OC�Tf���xl��q�{'�����/TQ��>��o�T�������=�h��9���9�'/�����c�d����
m�R���{W���%���J�b�1�����NL����-dk�<}zAGv��U�=;�=�@_�[�5^q��8��-���]��5Q���`N�5}�YQ)��c�F�y��]����D�����fs}#���N:����w�Mbp�R������G��{%P�a��B�W��4�n<��MF��~9��ouf��#�;9�.d_wt'/����t_5s���3;1�[C�����������r�y��������A�0uA�s�5m�U1c8�6��);�K��t���N�(ZO����Lm+3��������5k�(�U�wn&�'��5�^���/��Vm���@��{VX��]�"7�������r����/p#��',r��,c8��k�4�ymY�r�����|4���m������Y.���3���O�e����}�l�YAaw6n*���pj	�q�� (��mL4������jl��\����r������*�%f7.F��0j���j�����:e�\�c5V��������'i��o�x���9��2���FPEv@ba��/*
t����3g-�v;W�JW3��+�����������j��M��-�V�8�<E���kg)�	��JM�u=��QN��Wrv�&����eR��x+���}7x���DWrw�n�cn��<Y���5&O��o�d8
��3Q6,�d�t��g�Ul8j+;1K��w.��v��Tvt�t^�E�[���lTc�2���t+b��������D�~W^]usp:T�����{v ����_\�c��f�H�y=i�8a�0%��2fF�o+���3�n�b�Y������m��1����������r\W�"��'E\����]w3���Vl�R6���������lk��$\�V]{b�ly�/_@$��7�V�B�|T�3�[�C�zj`)T�(q�XEu:���&cS�F���'K����u���+���������\�^p[On�6��u!�������R	�x+�2�������{�V�3W�[�Cj�=��y4��.2�p���T��Jm�I�E��t�JFcm����:6�K���������^��{*&���gn[����%�qX/?|���QGU�qW�i�����=����C��Z��%d����io���N�na�������l|�^�[w�v\N����Zh�h����l�����i�/��@a�L�L4kg�����75�%�tA����M�w]Of��'n{����]�[�h^���Q����&�CtZ��%���5Q4Lb�����������Q�����W��Y����2�4���7��5O�f����b)E��m�S���3.��W�v�[&�x
n0�,���z�Tfr-���T��b6�1����Qr����Y7�Kz�;+W]�z�)�}�!����k��@�|����G7�r�B���An�u�������Y���e�]u.���RS���l��7/k1���\���vw	,=��)�e�<��I&B{��:�<����y�a��e���b����S-��-��2�G����X{�zx��>���(G�+an�W����Zz�e8��6����9�^�����V�^����g,%�n�g\9�����Y�����{(���U������QAW�����{2UT�t�w���dz���\�,����	
�|p����j����s��HE��So&qL���H�Tk�nA�L��c�fg.E&L����-m�/FH,��L�M
S#5��s'��ml����R==r�����
&���3�����E_���fx��1�Y��4��(�E�a��4�.��:�����m/R5�}a`�w�S]1g:�+�����Ts��<k�[V�_'��Y�o+f���g��/�
��=�#���MTU2�T���M����
���GF�[����bv���
�:�`�k,��j���{fyV@m�vZV*���fvb�Mp�o����n���������6R�����W�_u�*Q���%u�k�kS����4�z���f�����;��J)��)���f�>��tH^Ft)��<����R����~U����{�fs�bw��������<�/�q�*��S��w��
���@HnO�������������Z��$C6�Y����K����r��i7@��E��������0G���L1Myf�P��H+�O9A���{q�3MqF�j����{�X���y'��B�_dW*H^����@���@5&<�����J�/^���=}�.�#K^k�aE���2����y�$�RX�S~n�Y����_����������G}3�;�.��{k><[b::/��s6oN����$���"g)�j�w4z;�{($m{�����.���������%��;��*���x!y��T�Ob��0�����Q!D���Jc��Sv{��[
/�b�s������I����.�����T`�inl�B*2���v5\���n�����Dm�����/C��������fm�������!4��Wj�{;+���"�!���S�ATt��O���-Of�<Gx��|����<t�Q
����!��p5MV�z����-��������#)J��i:�R��Q������<u
j������0o5uE��N�x�����G��F�^n�>���h�{�H%��x��v$����R�se�E��B���������aN:��^�r��Eo��uF�{'����9�Zt������3�C���]�R�t�����o���u�w�l���*X
d��t��nu��5�jMS��J�+0��"F�! 	�Cx���������L���	�GV:h��|��b�z������\";�����������n-��$o.�������ZON%u������	���vjN�~� ��n4�W,��
����TIi�%�7���a�ycY�0x�
�^�ZT���^����oK�~�w]��2S��&��d�)�w|����v}�Sn<�v��=KO����a�����k��p�=O��c�;M���{�����^������aP��;X��K�U�a�2��ya�&�,�c�G�����C=1�rA�'���-*�s������^{;Y��;>���^�.�-��a��=q^��pz��X����}����������/��������v�{%�U��#0�e?R��s��U���/Z�U�D���)��q���Q2�p�i{���&�[��Uk��8��p���~-7�i��p���c�v=���b�r=�<�=7���N'��Z1x���;B}9�wd7��6Z���BV)��;g�c��Q:�V��pIQ�g�K���!��6�O�9d�q�4��Z9f^���' k+��^�}8u���az�[�Oa**�*��v�zzA����f�����)�7�7Uc�Azn��\�6[@(O���7����o��N�_�����R�4�����u.�.N�o�����"�X��Uo���7'��O�5�Z"i>[2T5���#�vM�[+OH�>�1��������F��E�]�V��SC�.�l����[$5�f[3�.Ev��=1[N_>d���-�����n���������
j����H`���F3/�d����P��oq��:�jgT�h�c�e�p���{{)����U]��5fz��(�-���x��a��*J)$�93��w�L@oU�Y�z���^Z��D�`1��e��Qx�PohF�������%��O�Y��+-���S�v���Ds�reP������m����E��\(M�6'��l��y�e��Gw�Z�cCg���'�����^'"����^f�R�!���F�(q'�?c'k�/iz�� mV�wi�T�^������|@�����g	���7 �>��[v��&����*=	��z��%��U
���|�������8���L^�a]s���|�����@���]�k|E���Y�8d�c��A�!�DlI�eG5�����0w#�Z�|�Eh,����RG"���"(�!<���w���#,c�})5����p
w�p���~��V��%	��k/�W1���_u��:�$��E�����`��������)GnGe.�~W�w��Gu�F�W��O��D��).��rx��"���+l��=y�0r������A�t���Wwd�zEo��w�r�3��[Kq��^�A�w����Q7
���=U�}xI�}�Xbm��n�#�v��U4v����5bZ�����8���������E����g|C��Y���,���:j��'a��(��'��D���<��n="���XoT�s���D9�����������6;��e]�W��*���)���v�j��:��s6���K�9�y��)n�5na��%�����/:m�`�WRd)��.^�i�5
�co.�5������{��m��d`��u�������j/�3+�L#C��F6��:��C7�R��%d~wVQ����>�����^��S`��{A�����X9u��@�^)t�:]�I���P�_9��I2i��^w���)���������Z���.uzI�_ ��b�yeN8���u��~K�������>�������_��G%m������X�e%����NK��!H����}6�')C���]�Jv�0I�����[���l���#��q�)��o�|+�n2B������;���/B�2k����m�
c�b��]j�� ��{k�6�7��_�,�G����a��E�����E���sNa�X����$/�26�T������nv�����c�q�d2�������x�)"+���;vmW*�S�h�ui4�^�B�d�7��(56��m��������ap����Z���*Ox���}���,>�����c��C���1O��������v�����^�g��Kxn	�XYGwDsz���
ryCR������9�����F���w��������L|2rN�3k�:��@��7�efo��r��E��Y��������������Ob��N�Q���w�"l������>2?i=w�f�������V]s�d]�4����t(�o3)r�R�@^e�z5g�N���ew4�n�5bvv�T���m~�V����O�Rh��oa�K���,0M�h?%z�#����CiT"�6p~�F�=*�N��M{q��]�����B��o<����jm���
q�Q�q����o��:c2j�3�+LW�mkr�J��0u����0"�3����!��4�����=��j��A�g
!S�Wt	��J�//��T�Z�����X���R��sOc�Y��=�Zyl����|��x��]���?]�N��;aGi�c�����!g������i�����W]�.���roP���\�����s�����gO4i6�� ���:��H�K�:kg�h�P�����%�g���JZ.��Ng��vz�o�k�lD��&��Wm�u�{l�90��'V��J]]A�N�M�����k3(���"��uu����X}���B(�"��,G{����#R��[b�R���^��K.��uIQ�������������R/�5]�Y��
�q�iY�:���5���;x����Y������Oc���u�������oj�Vwm�!��B,��5'����/v)��T�b���p�[i�xI��������ufl@��N����E��XZh_��������$�"'lv����32���$#*�"�A�@j]���I�_��7�$x��7�`yg^p7�k'5q�kms�����*+[h�T����'�����t��T*�5�-�z���s
C��G6����x,W�~�9z��5p��\-�H���U=%�����Z����`	4�E�Tj]U"��O7/]����_9��{S�!�[Q=5x�,	��v�B��k32,�S%�zt�
����YI�[����>�J�W/z��F�"r��J"Oi�5+xIb9�n���Z���������\���S=�{o�y�'Z���N�pb4�$�����@���j3��w{;�*�j���y^Y�Gh�Dq[��k�o���ox�W8C�kN[����K������(TA=I��\j��z���o��:��Y���x����������G<�D������
d�-��uw��� ���M���O�&o�Z7��"���;���
��f�aJ�N������FF�<j�ie��Y5�X��5�U�A��!x������(}��~���ym.'[����_]�z�����j�c�[���ag5�ur��0M���
e��#���u��g�Pnz��+��3bPy��*M�������;N�KF�u��S[�:���#��w��X��8�="�N
E�z���Fe��������0�4���X[I�� �:l��G�]L���T�.�e��Qw��'h�#_M��X�I��E����|��g{�y������b;V�C�M����.����^6;�0N��nlN�!�������]u�5�����D��#�;9u�R��n����zv�%���Ivb;Rx���)8��*�+�rY�M�f���d\�b��y/YW���#r�*c{HgX�������J�& t���n����(�S]vv�9������%��F��=�z{n�|� {H%�����`;���.�Bh�j���h��d��������V�I����Lu��K���5Vqc���=�
d{�09�� ����B�����������v�5���.2��Jw����fd��u:�m�#1��������c�U��$��:uWy��aZ���
{�m`�A�z�h�[,�0xM��
T&��W���v���m�u
�.��a�Xy�,��aLg[(\rt���j��Bnq����,�G7+�G����WI��m�%<Y%�y�U��O�)�-�>�p���*~[u��^VR�tL:�;;dY���{a��S��b;N:�F��?E�T^�'�k�P�>���`Q�`�}n���n���s
����;�������D��-�
7��Z�������=.�O��^#���(���[����7m���R��#�_0Kf�!H��k�#sBgUV��F��!^k%�7�h�'�#�8�
�y0���G9��:��m^��'��B,JOK<E!�������J7��w�����oa�#����j�Z#�
y5�e�jH�re�jy��U�6b��i���qx�Gl]S	�fo�����e'%�9����J{����K���U����2\�1���
6�*����;���+��-]����-Um
{W@��5b�$����� �\f����E��T;P���E��
�/_��]�j�V��|��u�3��su���.1�5v''��Y�e�
�^p��B�����ZV^����3����������&,G�!�[��v��=����m�����4l��Eq����}��Zq9�7N��TS�}\��G;�f�P��c����)��9*�;X�u)�.V�o}p����B����8�X��o�QV�����o����sT�5���c��QV8��T.wh(,����J�5�t-�w�1��2��K���w>�yR����R]j)^���Y����`ab�&R����k�����
Oz� ~f�K���j�+��Z��+)�e�z:B�E[#ef�8������i�UN��)�o��1�e��Wf	O�V=���8����L���E��wM�et�J�%���5*���v
V:O\��u~w���x��ZiOz���#��C<�Q�Ts��m)L��;��������>'�fB[��:�[�*7[Q"�s�G��xU���p_����yE�x"\(�t�����.Z��Lv� ����`o)�]h.�:���C��V����<3��=y��-���{����N����,$1����� ���y3�Y�����]:.*�x����"W{CW��hP�
�]y�u�{|dZ��k;�~C;3�f<5d����\��0�iK�&xLRw�W���
�j�C&euD���t����'{�.�',���p�W�Z�>'������0�OZ��)�����3f1�m���u3�BQ��� ���}g�l�.+:���~�u��v}9�,�����l�WA�	:���smD���WzH�N�QND�����o)�������Yx+������?WDmiu	�xZ��|��^�zb������'�*�j<gq�������X�L�V�4��� �Kx�m�U�bz,S�t�������/-'8�S@�������7�c�]���^imT��f�-�<�����G�z�fNB?J��\����m3I.�]b�<M�I�G��fez��l
��mfQ�2���_�B�/c�"qk^@����R�+��uR�TY�&���������_M�RX��zW���
�m���x]������ P����vCNS�ol��v�Hid/;���(��7^y��,��}�r�����p���]���*LLT�/�U��<Lv�*�{�5�5��9��q���X��-����]��#]�gmQ}Y	����7���)�CF��hRPf���LQ�1��x��y��&���\�XI��5�����`y-ZO�v�$�	���Z�'����d�{q��%�����6���b�=��6�e.s��������mfP�z�Vs��pv��Ga�z����������yJn��Z�C��6]���c.:�E��kG)�*x��mWeO�#Fqu��"1��"2�)�t��1�,t�SD�R���7}6>gi�������$+rpCAW���'�����P��=FI:�����e���^�3��6���/��Dn���5u����(T������o�
������(6�?��_C���v�X\����I-������z;S�`R����VT�g�d��uAP�^�E]:���P����p��|�
`����s��4����O���^���I�������)Op����oM����%�m���l�e1���*�:W�FQ�,�A����o+����h\�����F��W�r
������z�Z��~�Q�m>Q�,�1.����o3����%[��W*v11}�{\*�B����*h
}9U����ECfb��4y9��<4dy:��d�uo/^���7�$5ki8`��9!�7��#TQ\���L���U|P�S�,���.���R����OGq��H�tv�R����KU��.�>fo������r�;�5��c�v�v���K��u�������*���~w7BS�s.zv��9J��3���S9��0�*��y�t;����z�?���������]1�T�s:+��[����7���]x6c�|��u��!D������a����������B�7�+�K$��m�7��nL�-�H!{�y�r�Wk�&lH�����k5���OE%=�,����C��.��=��	�AH�^���G��������lP'S�2e���*�=55xG*d�Au��T���������dA}X&1f!p� �8om�>��+�\|�����:}Y[D����s7.�j%�uZ9zz�k����r
zQ����[/��q?
Ol��un�����&��
K�j1��Q�f���v�K4xA���~�l^T,�G�o���P�G���](Nu�K��5=��X��tTu�.*���5e�Nwe����S�h����W�{3��%k�8�T��cR��@r�5�2�q�k��D���F����	�!bV;�W:��=���4�������m���+���Z�y��3���A���:��J_:��6;�����v�~>�$i�l4}7����D?E��^�	R��<�H����1���������r�c�B�Ru�A�����OBgF1���|`�������/�<�AAa�=}�����z��I�8�VZ���1���.��
���*:�/P��Y&z2��O����t��]uy��ds�7�oEw?K��z���A���a����\��(�J��O;����2��f�_�'i��dLi1����FOM�VE���o�l�Wr22}]�E.D+����b
�'b��8i���7]�&�sJ��"��5=A�Y��x���G<�8����������t�K�n���f6z��]����&����#�����w0����]���R-��i�u�m�Ss�]��j��CL��n����?+����5//Ib�l{��W_�G�^�^��K'����`��3�b�O��*�����������NESWA�"��2��V��Z��.�u���*s�
���4uf
�u���P�x��3��8�`��:����P�	����C~7F�q�:�kW�����*���Y[�f?m����������
��;w�X�;I���4�,�������S�_��h6-u��,�W��gb���7[#� �g9i��+&L�.��Z�Z&�Mf<q�n%��9$p�CW`��N� us������������X���T9=�"!T	��c�5����e���IJq�L��0����]��J��g�W�����+2�O_S�ybYv���g<�H{���m�������6��������,��n�<��V�d�_�G�� �a�w������b7�C)<�{�w��������V_���f�q<�GA�YR�i�,EMJg�������-����+���fTX���uW���]J�����+��s��<���mA%v��+����m+���*��J]���F28^�k�;o�u��f{A4�vDu��~��I������l*:R���������-4���<}Z%~����2����j�[^�}����y3
��//!F�^P������)�\7%����K�7I ��A���v������N��^s������<����B�7�=N��&����YjU�5�l������f��AS&j��^���]e��CE{�7F�E���*�Z������}����WP��\�&b��[�k��G�_�_p6v�}0����U��G\=��+�����KKpgr5�Z���7.��b]�|�Q�O�t�4��>����I�K}��7{����b�+����,�Fq����m%����;!)���>lf:�S�Mp#k���}K�L����O0��7M6d�����4���U��H���q���^��6��K��q���,��e\{����)	�__h�wZ5��gU��uU����Y���v�!�����z�R��^O�]�4�W8xe1������<�kw�&��;l���+Zu%���"�z�]W����,�qe���^#ES�t�][g��P|�O������3s$�Z�����_���~�����rA���:����T���������$����Ek��s�Y�{�#^*��T-[U��z���������>������9����^���t"�5�9Vo�Z}:x�k����������7ij�Av�1E������vU��Q��*����N�!l�n-w�]�eZZ���f�|3#���MbX���p�/Nzv@�yR/�ci��Cx��h�sj^g9#-M^U�>�����m���`RV]�6�<Kk���i�q*���,7�n�[�r�qV${T�R���2O���<��}]Y���P�N��s`�GtD:�8L�����<�m�GG���
�)��Wx�m��k��o��hu������m��c�o7�T3�C	���(�GwWV�������L��_-�����;KQ���-�%pU�	��X7���a�m�E��1\fC������c����'vD�f]�1<�=��i9B�F%M��a�G���u]���67�Ud��n�1����>%9�c1��y'���Tw�Y����Y��.�<�G��A��S.�����Qw*�(������:���;0�1��L�:�!���fg�������������B<
�����}m.x0w����7VW���Q���w���<���^o�E|�����4z�m������gL�k��
��H��%EA�Q�����}9�]xrsm���c+E�t��@s�R$��]-SQ����k��&
�]I�^��q^D
�:s�;X%�o�1RZ)N����o����Q��W���DX�/��zCW����u_3C�nf����oa�
S�s�}U���&�c�
}]�0������z��y��7�rH�,����@�V�5��!�M���'3��S���VuDM��o���`��+�v�;��^��M5��)��;��
��$.�Ucw����D��m��t������ie��8Q9������Y�m
�]��|t�B]���6��^*����W�����Jq=�.w5	���8�e��.�flz3�������RMf�HnP���c�W��Z�g�������=�5�z��.;��z�+ol���U��&o����FV2�t��\���o!�>��=+��vRwi�v�kk��QJ���v=�n��h������+��D�C]{
5����R9�M���e��2^f���W���h�<����SDY���h���z��n�������	�b��R�5x�#G�X��V)�	>��'����K�����[K���1����t�^�1ev\WpcpAy���Y0R��Jz��u?�� X���QY N�o�9���tA��*�y}&���~s�
����$�����%
��%wCUu�����*��>�k|�����vK��7�Or���*B�C��~�����z��6,I�L�{^���A��77����LT�+��/?c����!E���u���N��L��f����`������&o-J��(�
j�P���b�P����xj��+�t0���.���R�'������|�Q/��u�g���-��:�X6r��1��+��v����J.�\r�-�
���m7qn
������%������ �Z��������i:���g������PJ����W�'��f�/D)�f/x��v/mK���G#����mx�f�����{!@��U�X1�u�����7]-���a�p�q��m���	���Y=h���5��d�$S���V���~��������lR���B���\���f��l�q�67e
����`�`��x6�m=����z�d����f9�m\2�cw�w����@G(wo�{�c�
�`��T.��P�5a��J�=���1���cx�n�V1`�����8�	�U:����D�����*K����8z������j���O����jgUX*I��	����&�-T��s)���vT����>wB�����T_�S��w��*��gXW���R�w"���Mn<���%K������]�A?�kk��)+��1n�Hc���U9������h���2�e��7un^���� ��0s1�������h1��K�t�����]uM��A�����x�����M?���9^itc���*�^�v0i��c�F>uE�)L^cy��p;�{�S��[�<�a���z�wR�rV-S1��P��]�Nd=��qxr���;������0wg��#��6x���������-��{�����s���;1������o��S�W�����gG�k���sT��d[L��<2��V	7�:-,z�fm{�����x(���D�����~]�s�!��T�1{�Y��Z����!7QX�"��-�4y�t��;�UA�A�=ImK���oz.�8�X��Hi�{�o<�c=Skr�F�#6[-�e��o��j�f�����vl����KJ���s��|����Jt��*S	�=��Um>���':�4����Le��T���h�����u�Q�*bk�;U��y�T���Y�rm\YOT�W9��U����������[���yM�*c�nk�s����2|�kz���7�3ht����Q��(�am�/�����u7�PT�r�%��m�C�~6U:����w�v�R1���t����`{5h���6T�,n�z0:��F��������:����4��)�k�(�4��3���c������GVnAC2z�1�B�<����.t�b����.!c�a9�����j
o��1����yQ��D���)r��-[xr��O(�W���Y*���k��Q+Ola�Q�e[�-p��������A�j��vb��o��9R�+e��v������EY�c����
���	h �c����s.t�o�n>��`���U�=������o�2MgBF�\��3��[�xw��+�Z���g�a?'S�����r�����#��������*���q�o�%���y�������}�E��z�$��OZa���:�����M��M���	��,����[�7�;�bM�3*rOc�`�1���*���i��yq��3��!����D��r�<�i��du(p0
`��%�=r��}{����d����c���F�Q�KzY�7L�����*��<�������|e�z�u`�h��w+v��@�_Mho��K����J��z�����T��
��u3��Ok�����5��r�K���A�5�J��F�K2�?v���{i���\qP5*���/��(�=.~��c�����$_{�N9:�����FR�=#
��X��7gF�(�t�e�Ei���V�bT�� ����'�w<%[�=+r6IT�S�H��nd���}-�1�����n-��4q�kA`���o��.������/�$�s�����������z�\�>�}��Y�6��KEm]u��s=��^�gYv��&B�#;�g�Y^JC���4I�N�$o\od-&��]G@5��k�"��%��bn����tt��ZN���������kk�f���J��9��mV�����8��Qo;��2.�@�yy����U���w�&�1
v��0�e���<��g8������h`)�.8�����#�q��$��w�nw���u/`�������,����@Z��(�����\����q�o-xU���>���r��a���J��r�F-=4
�G�{
���A17�+sE�����f�fe���x���JXL�8����Z���T�+�;/��_A%�	��zs���/QZW�����f�J�S"��9=�����e�����1��%�^o(��v���*x��+��b;�\�&!Au��Ty�J�,�����2D��=�t���F��������������\��q3SW/z,��Cf�,�������A�l��9�9YN�$����gr��=L�8o��V�p�%�.�`�����wN�n��w:%Q�vlV��3����D�����ZF��k��<3�oo]4Q�@���5M���/W[�6:����
�L�i����V���5]���vX���r������h�kp�It'��f������T���:9J�HS��x�)����JI��w=��k�S+/;Zh�'���
*������������rt:�����Z��r�=�<%���s����e��:�`���E'/��W�w����]eW���J�zSxn,����X���L�;��QZ����4��8Au��Mw���m���V}-�[s�2����g	���8�M�y�Y�{$�j_����9g�zSs�]Ha�x�������Y���b�5����{���.���U+��uyY[��� d�+Z��SX��9A�=���\e�*�f�c��K�f;+N{�{���R��u(��[�����}Vk��L�{b�B����%�U���(����"g�NUl�};*f����hQS�D��xz���.w������L����2�4���J�a)n{8m0s�bj/����
��'�'���}�L����/\�~D�uT�����q�,���n���YbH{�]<�^�e�>�5/]�j��|q$<dL�>��=O��x����N����,��	$8��n���'N�A�^v_N���m:����y[����&�K��{�c���W�*1�o3Ld#u���*�{+��7��e����u�����]M$w�O]�<7O��P���^�a�=�=�jn��n��{�:�wrb�gb�����i���r��_��w�6���"�7��UC|xS����WI�^���T)��b���	��{<�]��oM��v����t-a�Yv��)1����x�uH�x_�1� ���wco�S�R�A��+�`���
��c���%d�O���5�z�������G��s�Uo��5�g�X��b�u���������
`��U�����I=��W��<�&�2�	�]�,+���+��l�Jb��
�������Q6o�����5���V���J�J*���<��G4��'�#i��|w��
���E9�]�dw{��?cy�� �]KHV����7�I��W�C�4�":sf4V�����������&����%�U��p=�����-~��l��W����%m�+��@1���6�@D���q+B����D���/	o��������-F���J���O��m��o������pu��%}������OU��7���A4�������-m2����=~39*�o{^^On�OJ��E���8���.D\V
�c�]f���0����t�g8�b��^o�}���[�
�(���-����=}�
��I��J4WS����Me�����9�<����������S���rN��� �{@�����$z�����WDlY��}S���[5(�%�|�M/{D���rz^����#�t��3�%v����r��9>S���0�y��s���}^�.�WKp;���7����$7�q����o���c�z��u��#ZN�{��;=�D�i���wGB�L�{f�<G����y�Y��')�A�c���P.�c� �]|��!��J nU�~��6��:�$_J�v�=�����S�T��������:����������R�*�mc��ty�4n�Bm�
��+C���9���eh��`�R�����r��%*��9�\����{m(���(o��t�~.�x�i��ob�;��E������B���������7/���n���9I>8�D�f{x�)-b� ��c�~b���m�Y����$e��a�U���=Efl
X��"���U�3U���S�C���7����I��_�����U������g:��Z>�4;)���1�(������B�e��WrVHO�i�H����W��(D7J�������7��5�T�]������k��y�sG& ����vT���'��=���<z<�`�Gy�^�5A����"���{�^��{�|����1V����������S�f������RaJ�x1���D�mx��}8���A�M�F������;O��`*L�V��^�����n)�L���0w"�;C���FqJ�J����v�F�u�ko��F�0���H���H������'��0o���=^"���K���B�Y������ZL7^3��oY���4��7��|q�*�j��6 �w����`�h9����\j������'���
]�����.����F�>Vx��+�b_�.g$8Wf�6P:�|�U;;j=�*w��+o=�����X�e�4����X�f��V���nb�]�QDxU�f���3��tn1��D����j|x����I<��; ���ii�v�����3�=���Jh��n�"9�7�J`*������q6�Li���[�V7A������HjZa�X�Q���!�\�4��xP�r�vjQ/s�bN���]n���N�����F�c���e�jVqh�a<��x����Q���.�kO���n����\L��8�dR�j��B�S�g�-nW<�z����0+�wx�:KX�Z�t�H��h���z��e)��M��LOCv����8������F����<Q9U�M�wV���EE�W�
��Og����v��������w�Vf����C[�X{A��fB&�i%�����kc����iX��Pz�"���������opcSV#fR�w��VEe����A�t�3m`
��c���[[�:�>������Z�����0�D"�%�q>2�n'��n��+�C\���j3� ���z��V�v�����������D@�Rq�tgL3l��(q�K��:������+��
��g����=r��[�5���Cw���w{)��.a�1��������Lp]��$�m�t}���L�LUG��g�Dj���_y�bcU�����r���p��q�J������ur�f��v��x{e�n����R��6x�s�w�:9��{�Y���#�
�X2�Q��4q���m����T��qW�.�
���D��tU��n�K|Z��c��Q{�������9"�tJ�H0��(u,t���`VQ���%����yvi���k������$�7]����JH�w-��k�s�zYh���u���<�=�fknZ��b��Ok����fgJ��n���$�c���-!������=R�A&��g�M������#���z�k���f~C���2��������k)��/��j���]l���h^t�0��{���ws���>-XFe�.���oz���k8����W�Z������]&��Z@�b�K������V�:�s��bs�Z�����xf�Y���v��+o�/��c���o�h�9�,�w�J%��Uc�nz���#w�F&��7<L�>����[.�����N���2��K�r��V���E=����������8���y�)�`�a�y�� ;���Aj.R�.�Q-��gW���s��,��ngyAYZ���a}3���B_ ����^T1s&��7hCd%����������*������K�����Z�NLc����n-U'���Hm�%��0F��n����r���U/���n��b�yfS�����6��n���:�����v��3�+�F����x�%��z_*m�V���V�I�O�9���w+Uz��/�et.�`��V\�2���2�������C�l�SR����oiX#@AeE�O�����w�-�(eU��\ �.��� ���|j��/d3�lX��KR�4���A�����F�+3z��WT�����k����!*,-;������3h��Y�[�+�Cz��7d]]�p1�Z���P�q���.fY��U�����8g
J�+w���f��6�]*�����7�l��A�Q������r>���t%����z�����^>�����q�sh����f,�
���T����:�����n�P�w���L�����
0*V:���P��=9�5��B��r�n���[��Z����O+�>���gN�����`q���neN���M����}Y�S���u�����]����W>
W@r��UY�6a��WN��8s���vB����2����$���}=E���s�
�}��Jvq���T%{zS�iX�	�,�w������|t&#��6��(s-
�'l�������B�#��:����S[��p�u�W�th����n��9��+�V�m����>\�<������W����e�X(pvv9�`rK�k|!s0a������Tv�Y���^,S�G�z#�������Y���tPC{G�����2�ZP��<5^x$rmo��7��_1�"$��/���i���#;6�s����v�q�h���a��'p����z������VX�j�L�g:5v����\IzX�<o���nzs[�g��\�(M�#����j7u�jw�<�[�p�d�����\2�����-��Ev<D:�eF������L�������nq���7J�:�A�u�]N�^�U|t\xf�i��<���Y>����CM��]���WR;c�&\\���N0r\�b����.hl=�d8a�g�1�����::W��[�4'�<����a��,����3k�b��G�|�[�N��L2��T��Zs�%�J`�
*�/�Y.����KY������ ��c8r�]5p��^�\�8���vr6�!����M���+��m�Ty�y\(��m7T����k�%2����3�*�,�c��=N�i�����	o��gk9C��iP8���e���[�n�ow�(
��0��q�j{�����Q�����n�gp�Q���=����G��5<��������w]t]����$�������^z��#������XaYw �=(��`��S+1R��Z�C\�F��q��>�is2N*����+4e�#����8\���873r���(_-*W4���b���N��+TVu��Yg�!��fX�.0cy@��T��f�:
��#��\*�����������aa��m�m<����������v�c
8�x�`)�#���N��m��5��R`��#��N����Oc=�-��=���	���wf%Q��{{�E����%��T�[����6�'���{�N,����I�-���
[~[O�0v������\/:�*���K������2������,��%�H&m����j�bf>!�z����^�]����%8��n%
RS{��U����9HV��m���2�)��{�Uc�����Y�}�OH���m@\���F�X���
P��	d��D�6�t��2r���h_!=�Z��|U��"�:e����ym������3�<�.6��T�g%�����*�=�t^{�0�@6/��t��b���m� F /��[�Sz�8(tz��9��3�D����ec^���&3:{B�g^d��3�U�}��(+j�R,�s�K#��������������%������l��n����G�5z���/��������:�2������eOx!��;�<��n��m�F�������,miQd���
���NH���L
����vE7Z�����&��'w���&��'&��s�Y��-�����j�e^�O�]��b�Z�UV.��Mb^����y���`{"����u��=<]mI�����z��IB�������'�������g��>���Y���K��k	K��d��0r�{^(A~��{������^���V�7V�����s�q�b_S(R����e�������h��#C���n�wt]���������]6d��:�*�o��a�F�����|O�v��������b��+5�5g�7�:��^��i��{}�]�"\�7mZ!�i�c�gC5���D7�=]7Q����������arkOP��xR��fRN�V�	:�,7q��^����k�e�XKsGb��4���m8E�S�+��g��0��o7Ok���*.���z����=l����s��-~��������C�cI��9�B3F������sX�g|'��s3N�T��H��>����+���Z�P��>��zoB2������E�q��d`���������C���^{��a!W8��Lk|f=6�������_��\.F�hM-��������#����6�XV��B���(���
�]y�2�7Z�� S:w5*3gn�z�5?_wxU|��r�k���mg ��B���=���M�VQ�A�QP���*jr��z�s����B��~Y=��^H��T��L8���7��������/X�|6���r�-��x���� D�4���Uy������Z�6
�}���-���CjOfe�@��H^We�LTn.���8v�����Wy���B�3w:��Lm�WY�&�y`|=�+o�\��5����
��K��<�j,����>��E�Ta;�a��'�kE���:8K����`��~�d�����'�tl�8k��sV�?Q_N����}�w�^��ix����dk�ti�9�!�;#�o[���ax|J5g�o$�S�H[���k����F���v9D� �UfI��<����������S^"���W��v/D��StZ�b
\�����91�a�g���`��;�����1.��^������������]8fs7��M��r��t�d��)/:ag�tXB��]��C�}A������J�����d����'~B����]�����s ���X�h�ke�%�\�����IO�zZw������!��v�B�k)Vwx�������{��(�n�H�n�"9���	Nv7+g��;��V�=���Y���g�y���]�.2�p��b���P�������7}lH�E�Kij�����	�^����{�E�Jvrf[W��1wn�Ff8�n������q'E9��:8�v0/�m�{\��������������k/���e�jm>��Rln�/���9l������;����:*��x�yWG��n�2�r��� �7,U��O�\l��g9$��������p�r��������Tn2*�SM���%ME�va��3������5����������r^�'��r.;Y�g��� je'l�hu��w��Po����fzQPEE�^���:�����@���Y�sO7������\9^�W`E���[i��
��L�0ZW������{����E��P�us��RY����rT��	�[��n�r�O�{��G��na-��6.�{0��������n	����E�|[�������<��=���OR�
���g�nuJvcs��O8x+�&������T���b?��~?/��?�&�6~���~���_�^Z��=XF47��{$����
TO����
���������Hts�k&:9;��H�����b>��X�uE�u[�%wzD�
J����>n�=(��1��p���j�ukr���'���6�XZX#L�WF�1�O���j6���LQk�2L�s���dm;�1l�c	��c�y�]w@�s�z��	�X��Z�;r�N%res������y�tmwS��.�<�9��7
V%�	�#\�5}}����U��B�&7.���n�{��J�-��D��w�&'�F�Z=�*�f$�C�R�P�����O��[��bQ��(��
���DOh��o8��}�F�
45�(;�����`�R���I��7���:^�T��S���rQ�t����m]R�=�]��[��k��W�Uu�[�Uy����������9~^��`���������J������'
h7iH�u=$�l&���T59Z����:\h������n����������k��~�����^t��V��|��o�����(��E���okG\��%N��#!�E������c\Msz�6`���5[#m@���������w��������jO=�W���o����P���
�D���{��4cW��&���`9R����������R=������B�P������W�M[��sn��gu7���$�l�YYWsOv>+��}8{���q�[����3�4��*�n�Jp��+�K�t&�,��������8�R()G�K�_��mw��{tkB�w^x@��[��c���9�y�/P��������K4��Q��nUo^M����(����,_�=���4I��q+WX��Hs(i�F�a����J�~������������]�]r�#Q{dv�x\hJ���+�D�fGt��3OoWG�������ejX����z��,E{B}OgmE5^xxG|�=vy>43W[�Dy�f��;�dU��U�"��2��:������+�����`�,���=�������Q�/�9�����YI��\wXVr[�8f��U�-@�T���RZR��q(d���^%�h�}<����P�eJ�����F���vy�H���,Z()�"G�������{@��'(
Wr����B\A
�w3]���2�Nu1���%vT������qQ/J��4"�0
�!gS0v��"�n��]U1�L=�2K�Q%�s��V�<5����jm��	���.����}r=!!I���t�`�=�����kF`�B�W���~��v�������(
�x_)��L�,��c�j3[P����-[�n/z�-y���
����{�d��^	T������I���o���U��q��{:�E�������7+}��8�
��p�2����iO��V���YF���"�.�`z���5T����>�70�B�
]�����/�6dUn���	{U��h7���2�;d�<���=Q�U����^�6�}�l956���A����X�����Rh]x�����.^�[~.�����+c���v��dE��"�wM����L�\-��\`{��[���m��}
�s���<��pK��tS����#�=�^)FHg�u�������C&�:�K��{v�]����q[w4$=���-����8<36����N��iu������^��,m��^-��N�.���vX=U4�X#1�����iW��]������Ox����:�E%����=O�$�P}�|		�/��0R���zi�h2���aU���2)������������1u]��`��w�o$���OnJ�M�_�G���b���u,��
'�k�b[�������k��>
����n�/��������n�p�=gs�P������&��_2Q��[��OJ��?l�|H��
w�����O�7AG��wL2b���\��J����TG�%QR���m7�T����z�}!tK�u���`����R��e9���W=�VL��1\�o���0��W�#=�2��hN5G2������v����n�o-*A~b���]���e��F9|�64�|4�����e��%}n��{cl�j�����9����N�.;[6�T�
��DL���1
����J���.�7f�����S�V
%��Uj��N����3"x0-n��/)����Mu�vc��w�M��F���L�,����������m�#�yQvs�����o����-��=��;�j�*�-0xd��=��cjD.A�����������\�CG���u�mu���h�v=������T�j����W'��v��`j�7���/w�����S:�e�3�j�}��RA�7���*�V__��|+�&����o�h�����"�iwwd��ZvV���<YS����y��n�������e����q���V�
V��A��{6�L����&�.�
|�9�X�a�����u
���X�[��]��9�e�
������n\�2�;oj
������Gs�*(���X�JQ���y�f/D5�'W9�9�6�������iw������k+��l|h�N���od>c@�� �����rGtn����r�l�P��V�;��#�^�8��;Y�^.Y�77_>[�'s���Z1�|N����d�����M�U��5���c����`�

�
�GDQ�g��cm�d-��om�1s�[9ckU��ruWK]�6w���<{6&.�,e�|�n�m�#��c�W����x�w�������S{���;z �S~����[6o������T3{ ����A�{O<��������������N7��I���	���.��t�S/�R{�Ey��+k�\��=����;��.f��{�~W
.��g9���b�x��mFyIG�z�A���������
��@-����X�)��Lx���-[��4�����m���k�u�����8�Z"`������jG�P��5�5.��1Zj���z$ ��a.��}��V<�3��o���abw��H�1�z������y!*:��}-������|�Y�+���U�No����d����%�)���,%�����|>�{����}��gv`���,�*���7�st?���Y�����������d��S�vv�X���-����us���o�U�<���'Az�	6��q{�u2��^��-����,��hm��AV�$e����l�G��V����63����)��!�=�uybck���cw(�)\z�Q�k�����X����snf�'�^��n���N������`��-��������|��Y+�<��	���!L/^<rkCr�d��(�b�0�w����ml����7���="�m_H��b�{���=��S���#�Y�L��6�t��9��'h�R{4����$>�h�J��	r��/W�B+:%�_=���1�1S]����(!��:�j�X.�Dfn/�<qJ�K�����4"Z�}+��9�;��a�Ct����3��q8�8$���V��uvX}��%N�N
|���U���������
�z+���(�~D�b�����sD)����Z
m%+d��8��o3j�^t���br(���<�;��Z����yK`MP�O',u\J9��f^��u�n�d#6��oq���?{7g�:V�cp;i8v�}��7�g#�}�F7]���z�s�u�Kq/��W7
��%�v��-��ok�yv��i���M�*�������X�%�}�s�Mg)�\�=���z`D��n�Ux-�����B^=�,�J������Mt�����p�|c��7���vgy
cs�s��,"_�A��FE�0�������Tn�^�\
d]`���\��8����m!<�no���������9�r�h��>u����b��,r�2��o�y5���G��S�����)ld��
�6m���.]KKG�Y��l*���H��*-�({�0W�W_Y����1ME��26�m�Y����A�Kr5�����$4��Y�#hH���l5m�O��`��md���{^�#���"1Zo���U5�UEc�2��L)��{�/+!�bm���	�:1�i�u�(i!lo?������r.�g��,��{i�d�B%�%Ix��a#m�����{	�Z,�u��QqG�mI���Os6������=�g5�q�������=�i&����K�k���7����SO���6f����^�������U9p�r�GM;J���fN�	[�����1:�����;e�z0dV��9Y7
k<��X\�	�����_y>�X��9�����3��]�ss{_���������������Ep\�G�x{=��Y��$��~W�sE�$8�J�PJ���m��}�S/XkRB#�����j�1�-�baNo-��������(�o&U8�U���m,]1���l��e�s�U��&[�a�����Ur��d��/c*��T�3N��UiOe�e:�����qq;����d�%���F�M�=�0Sx�o�P��sR��x�g%�{|�����7<]�2��7���g�NoZR�B1oX��.	����;���@�\-]!\E������=������P����j�BS�2k�5B����nzd��y������\�0,�N����V�(8������2bb]=����d�e�<e'���9�d���]�
� ,��e��
v�oyj�t�t�_[��.��38(��~\���W�!��;dz��k����9���4FP�����`�,���+���*�g��@D�����x����l�����*}�@�>���+���{+na_�i�d�h�d�g��&�8���us:�N�+�Z�z2�&@Q[%�Q���/7F��i���o����,�p��6)���#QZ��U]O\:�c64\2����v3�wI;N�n,��L�k`4�h��k�E%�K��a�y��[����)���k�aZ�6
�^=AQW���}����~f��tH���8���{7k������]1��s3Z����L�����YZ)��I�I�)���'��G5�o]\��V��������r���E�^>l&��/����d�Y�\���iQ����L������r<�X���s��f��i��������[wNA	�b���y"����7��q\���B���my���1G0o�}���'��zjR�5X��y.��E����U����=��d�)c'lEyV��o/����2�Y����;��42:)=��2����]�.�wS��q��|�7���M�P�������5�
�J�1n���� O�T��q��)�7�OvJT��������o�v��u�����	���g��O(��Z���39�d�jX��t�B��;I�Hk'7���%j�x��&�](wW1}/��r�����U�H��b��lH+^����z�
�V-uq����VEg<�n����X�LUI��v;U�e	��W������m��\Z��)������LI���[.>����F�����
�1]���roM�J�Y��"�\�I>����������K��|��.$k��I�unr�������S�*�i���;��&��{S���j�V!���y�s��������n�r�����<������p����9�r]=8��5<Z��kZ�	�N���}O<g)'#v�u�f����e���I�U����u��nu��������d�,@�����p�VFeT�;73z�c���D��][w��$%��W2�Dv�\�G��&/����>�:&s���=���}Su'9R��u������]��STV���u	u�4���F^��O�����R��;���b����sk�gs��cr�1�K[��csZVd�y��9�
������Cq)��0�����u�b`5�i4fL����yp��37m=���!Od�L�����3a�F����m����`7���V��Ml-�[U�{�����QB1T����<�r��&u�n��c�����$+������(wn���^�S�[��������{4�qj��r)�R��Lt�]�@dm#|������w[�=<3r��-��]b�(���:Fht~��������r9�������`�q������y�-L�+�b��z��B��OO!g�,,D����3�)�J[{	�|�3AofP����)Kp�����]����M��J�%�2�[�aw�VF��F<�JK�w.��*����Na]�m��6�������3.`�|��Y�w��,���Ykr�[��v�{��,k����y�j����v�}�J����V�W���B9:=�Y,�|%vi�Yj]������mY��f��;����Z[YjOX0c��3OxX�v���^=|n���
��b�U���YC�sy�����Qn������V�U�A��+����6�U���������)�����++��'�#aK�����^;��hZ�q{�2��=$nV�lc9���:E����$^���8�Y}�jm�����P7�qoxgo)���NF���h��+<���p*��
wX���o��T?7x��d��m�#��]3�J�W�>�r���B�o��+���]�j���p��s'(<�U;��]D���1�������X�W=�2�1�(��WIh�m(L�p#�K���)Z�{Fq�4K�)Hi�|��}'Q���^j�u�Mv�J��*�xx8)���We���T����s���P��6kx(iP({�����f�����>������Q9����i`${iEcbf9�8������d:����'�}��~����{���y�b�RD.��C3q�:�|��-?{��G�mw9��%^��������<�_{6_F1��~J�_sUr,��:9��
��Vyg����p��\�a;�g2�����)�6��;����q�����be���(A4�I�a����M�<{��VJ�!�W7�;T�&���Y�i!kW�������q���J��IP��q�w��9<�i����SG);K�
����6�gPgu&�uk�C����^�����������|�%}��r�U�j&{�P�EK��:�]�%%#��C������)m�i���<�~4����
u�����D�H���no�BV�f����:��+/��yn��U;�-gpy]M^-��K��+|�S
��nJ[�WV=�AMs�{��E
(�R�!�o
s.����s��W������u�UEU`7��PKYp��~�:���{v�O�D�Y�����U��7��r����#9wOR�X������Z{Qn=)����+�R�	���������+���>�2�2�����3��.b��Yo:�a��f��e�������T���G���:Fwfb���0���-pb����j���t�^���_������CN[��R����6h�����M��������{�ega�Q��dI�W����_�h��u��m���g�4VJ��d����H��'��_�Dib�h����V��<(�����	)�zy��u�s!���U3�}��f�X�	���X}�����^��t�7�z|�t��<��V�[w�:]�q��o���sCy�R����G��b��*���G���p5�O���%�����},��3�������2����
`\����050�vv86�;����
������r7�:��5�&0<.�{�bG<��;�����������|$y�qk���.B{(>\Xb��6����?E����5��\{�o �V�Z:�\fXD��)�,����C��
z�/o�g�C4;l�����*�v�;���m,�����Y��d����t�`��v���m;`�[W��xk+�������*��\szhI��%��W�b������5M����p��4v�����q�|�un���:%&sV�:�V���s=�^�F����sAxQ]�X���b����4����S+���tZ��^�{;�g1+�\I�����{����XQ9���w|�q�)�S���+nxDU/����WK`Zl
��X���\��d�0������i���A4���4����E��q[����r�l�@oV
�N��[0.v'���^9����C�\�Mn8�1<D����d$]����"��u��
��sI^hsZ`:�8	Ss��1,d���jNw�$^Ht������xe�����0��C�Y���O���}��V|���N��8�Ob�#��,��{N�����^mM�*�R��ly�s�����5NW�L����(��s��;��z���l�t�~����XF-u����En{��BV��v��\f�+��rz���e������������l�����g�7k����xv���
�v���-�t�x20�
:��� �Q�y��<�UqO�^���k�o�m-h��e�t�s�[7�.J� Y���	��"��o��U�:u�������S���A�R~����+2�����1tn����_�f��kyJ�y���������D�����k'v�&����eE�78��*��-�L��`n<���
���M*	��U�JS��6d�;Q���r�]�X�=p-�]-�G`*��!Ff������2��A�XS�������W2/z�*�V�/M�aH�[T�o�*�?@������In}5��}4;��]�h�
���$3w��.<���u��=���BL�M�,���k��+��s�}�[*��A�+�:>�)>���{���P��[�<��o�e���6����F�WE�^��vLV�e�="��b1��N|�U���4����71��l���B��UZ��Xa�;Gy*�U�k��u��^f��K)���������'�1zy5W�oTC�v������<�;��������8Se���ao*����P�K����L%�*�9����5C7vPq���]	��V��a�Vj8S����n�
\M�~��3�^�����F��z�d��a�����[5#7�U��g!<�����Z�������m����l����1�Abh*��Y}���.�cW3�"(�������)8kcB}M���Q�;Y|$��&-f��p����n��;	�V���G��)&+]�����2���A/}�^>K��b�@���W�-�E+�.�L�Q�.�X�l��d]����C}N�����N*�����m��C9�#���C�h�3���9�j�&=f��R-�O��U<%oF�����N*�����c��s�!D��92�}�a��&�[��� �X�7q6��������_���|��e��O�����q�e-�����([�5�@�`�u)�v�MK!�[G/��z��������9[@v�;�_I��Pw�����W=���B�)�
n#�-t�g2�3kg�.��bKL�	0��T�����F6����v�b|L��['5�K��w��)_����J=�B�]�wm!�39�{��"~����3R�mn����wU82|�i���snBV&{��A�w�p���K��T�5����L��&���}���� ���*n�����>qW����h���r�3T�Rl�>~�Q�[���wB��OKZ�B���&�����]��eg*G���{,��o^�2��mt����Un�q�'��['�UB��jh�9�.�lhQ����-���GQM-���&L�Wn�%sX�|�@����q<���I��0�N(�e�f���'��	�;1�n������=�aMTUo����R(U�D���4�<��M-��F��`�����R�vDim�w]=�X��=[��acj��OX��#��5�LHZD�9������;MP�`�z9jZ��wJ�X)��1Yo:�X��1�����'�������;`k1�g.E��og/��k����Dpz�u6�,�GY����[��\F�oWN�Y��>�>7����7&������JI�P�lpU�W�X����V�G_A���y�9��P���bB8�Q9���.�~���u�����#���������zmJ���������ag�d�d)o�Q���x������~ZG�1�����#���w�����#P� ������������D�����:Y����..����,�v�Y�����F�s#���uW4�E��9���ru�������cE���-+�;����v�^p�x���}�+{n��^�5��|�p��.������dE���C���g����V%\��Dn�U'S\�U�
�'���J�����PY��
�����s�O���"(���G����)�X0^�<6�Ml���t�t^��>6���j�6��$����nlV�x_��U����5~�]�U�9^g���=�3�"j.��Woga0�tc
�q�0�f���r�1#���%���^�M#�C���]vV��8*�����Q�����8&Lf�5�
D���S5q4�����K�B���{���x���L�����P������-~n��ZvL{t�����!���E�G�����������������Q����B��\[s/�X�ZG����9Y��c���K���l�G9��8w���7��Pof��+g��
�wC�����}��������liw��$6�,�b�6��
��8�q�u�7P��||�;��}�dLjPw3��f����3�w�tm�y��]���2�}�>��|����F;���b��@���*�eD�j�Y���:����;�l�#Q�#��:���%}�,��b����Z&�����|0�K]/nvd�H�-S������~����1��+�Z@�Rm1BtY��3����^�����V��6�����������lT���D��1{��R|�^��w����r#���ZF�n������ksGE����c������8��[U��3{:�����n�wr�Yt�����{��;� ��a)�Avs36&�:fNi}�;yc�c.&��Q"��v�����??�����O��`��W���w���y%v��&;&�3j�f�&za��h�n�b���[�S�������F4�5���#<eL�jL�������"��4�Y��
+���G�,���{N�,x�_���q�Ob7�V���*�>A��c�3��"f,��+s`�2����L���0��Ui�e���-�2
3mld<
'8q7��*������z����b�f�ulp�!m��Z�[��b�������������������N�;;�QeP�dWHq������f�^���i;r>���;��;�SkNWs�q}LbE{�7^����~{a���<��G������	f���u<��7C6����^#�.����g2s������8��<��2�c�a����0]�A����5����L3t�����W�z��3���n�.�H��65�^��� ZU�BT��X��
�����RdJ�A�0��>���R���z��%cs���p�5F����hgi�Dni��n<}I�?b�],�N:R���	bjF�f����5F�M��P���Y���c�]�u�2d���,�Z�%�6���
��?wa�	{�6���nc����V����o,�``6�7�Tn��E�Wnt�����p3
�l���6�z�73K�Vyk�f#����Y�}���9�3���u�)�[��,()�mWfv��������7T��Wj��������Of���7z�����ex/�<�E���2���R�=��[�U#n�w%Ya��c���J�����8I��5w��CG.���`�=Fz�e�=��|M�
��I��VeR��t��}C�������t6
z�����z�!��5�������_���2���k�Wk8"��!��}�~�C+����P`��U���
fg�����y5�>����e{�����x5<�������[O�:�������guP�����h�����Jr��(ZT������,]-�8{��{��j1�bK�k���i�������WX��g�]R��E�	�Myg�D���9b�:�y(��J��z���-�	Mg8���������V��M�H�
��h�vn�(*�m;8�������>;��`/y������IK�����<��z�{��U���V��'����8�$h.����m�]��wbR��$@p��
\��6GHP����{�����������s��OWK~���������%&�K���.?�&�R�[�g����&��;B���'f~u2�����b{r�:�]�"S����p�M�{��`�u��y�F�A�6��q���^�����g���m���ry���#7��2q�Z7Z������R�����[����}�7�-�8G�wc�W�anz��I��^\}5v��������D�{]�9��6GGUZh�^��X��u���0�����v���'���x��qC[c�:�	�������rn�����3=B�����z�������7x��/���]��$�~�\����e���*[U�G��+p��{�������q����wr��B���G��S!����K	�s�fR��4������;��:����������}��������������|�f(v�lp���W":}�9�E;o(SUb����'�Y�|s�E6(���F�m�s��/�N63��;}�;0SO�b]�yos��97�w��f��"f������
��|�ldj�3TD����<M�� �s.�wiFfM�Ld����� X��*���/�W8���&�t�b�	(uHB���NTg]I�%25�&:����r��p��(�z�Mi��Y���nA�7w��Igc�����0f{ho�sX���a����%���a���9��:���S3W���d(�ys�q���B��a"k�'�t��G!����oMQ�t(�Q�������ex/]6����x)����>��^�/o�f���Q�)����!Ob��h�2^L�b�jH��U
lQ���S^����E���oo`�$.���3��|K��%������
������gz��<�&qs����1k���%��2���Y�b��Td����'&
�.�v<�{n�����������P���Y&V�������R8#��b���dr����<���hg�E��Eew�/��n	mQ��R	[���c�]6i�t�\��vK��T.L��}�R�C�6�8��f�����!Qz;wJZ���GNu�_��X"�`b5[�m'{�wV:)��[{���v6����%�uNRy���cN%����^^��N<����C��;�C��I�HyDv0�3<����xvg4D�����������f�@��_%�8���Qx��p�N�FiRp��/s�`�H���.���o9]]KU���,��x��0�:�����;�y�3!���7t������������xkj�+�<�M`�^�s�X'$���;�x$����X������8t�0������d�����q�:���4|}lsL5�*C[1����.��#z�H���WWNF�s����k��xA���N����O�����A/_9Lz�3�[�*O�|���>�n����]������YbE��y��������\�83k'���
�S�W$�
�f��N
���<���Q,�,�E���!�n�/rJ�����v���6�N���� ���x5f���Sq��5p���#2���y�]8�������;*���a�����������f�mS]�����7!�{�
���s0lQG:�u��=�2��
�Z��&��}]'5�m<�'��L�k/������+�^��UR<r�������r7�<����n_uJ��������`��b��W������6���q�O:�����;���E�45���bu��i7�����~����a.�_��vm1*���5�����������U��Q�{h���o
���3�kk��R{���=^�� R��/hU���#U7�V{�������u���Z���k�O0��e%Pe��G6d����_(��a�"���__v=X������-T��;7p����}�\�F�c�TKU2�|eM���}��u���==SO7���f�����;~�C3`6W�]\��������-�����0��-��8M��{3j���A����_����Zx�����M�v��4�5��9�{�g����bNS��.\=.gv��9g���N>�����1��m?V��:����7��;��\w�����h����%�*�^<;�bj@l%a����&q�y���N����}�k��+NR�J��U�n��j4N�����������5:1c)sex�s�r���w~��>*.�q���q.��Z�I�qc��72u^��z���wK�c�����7~3ok�R3b�"����
����������5szu��z�������g7C��n]w��'��]�e���I���5�TJ�ZwvNa�:WBt����w��H
n��^�g�������q���
5z�!�N9'Mu���>�.�a����+*���?�;���w=�u�Y`�9�c2��N�L!���X�n�/n�;�t#_��t�q���W�{��;��/4���.��C�����	�'����j���b���X����t ��u�����~q�o��!��'�<����D��;��1���.��������c����F_=�����"�G�M�����v���1=��������1r�f���<�^��wW����O�A���9X�u��F{N���������.�������:�-���<*c!=�@��n���������yjj|���N��9sj��;����s�/1�_�T���W������L7g@��{b���N�(�Ql���jnv(w:+b���{S���-���1���OC����U]X��p����p{��������A��+;��e����[��r+D�2"�}-��*zi�A}rk�TNY����o��V�c������wl>������a��U������z^�)�yA]��5��\��KSGlut��Ot;���m �;��+!��m���
�*7Y�������drNf�6�M]��q���=�D�4������k�����<��]:��v���n�Tt{nK1�c�`�-S�!��.i.�����������%�^J�����|���+�	��o������I\��5EK�`CY�%J���������v�*{4�Z�t�N��vd�_�^B���f��:���"�N=l��
��[�w%fm�EV�t�����C�	��+*e�W4�����t�����o��.Q��&7Eq9	�����Y�)^�+wIEP;�<{ 1�s����+�6�H�������C��L�|�J).�d�!�{y��tz���8�#�%na;��GNr&���r�q���}o6��^���P�c�F�EGz��4���k#K�
n�����l������.�
:N�b	�\�����r��PU����3h���7*�����N�3�y%1�*�{J{���P����P��=���u�wrE�D�,��8�/n����
n�S�sx��������F��+7�����&V��V����k������5��������C�,���.�.������qx�����?"��,m;��z��v���/O�}�R!�-�t��5Nf�u���N"t�N��+��E�c���g
���U6M(;l�S:�0e�m����Bi5<��R+s.�/���#mHvL���{�!H��
����{�:���������Xn�hTb�]��g:a�n
�^&l���|�Pln��\+1u�r_�zZ68lV��B��]��t:e	�wmvF�lI��Xs�����|�-�����KeV�V�$�z��\o,o�Uw���������LbR��R�����]Z��BX�I�Uqj|E��6pu��%n2�Oh��e���c��R`�^�b�b��N��9i��������h�����{��7��y:��cn��}�k��(:U����y�C-�����7uwF�p���n���\�6�����n5�����{�<��7f�neEQ&��.�#�����]�<p��r�#S*��s���='�>��Tt����^������\��J��4�i�� Q�o�p�.GD�z���jv9����&;�k��4���Y�#���6a�V����[���c
�:�S�F)"�5��B�V+�E�hw�Z8���r������{�8#����m��eR� Ia�9�H����
��#:����kl�O�=t"�hmLkG<�;�����;3`����{]�p��m��x�g����
x�`�28���w�d=�����	�Iq��[���Ey$���z�e`��v��S���M����n���i�~����Mt\wuzR���]�����0@JWio{�{�c���=/j��{��n�IP��~�K�R!�M����r�JW�k�T��T&�_g����O��i�J)����j���@s�>�0�^3�3��Qz�D�iL[M7���u��\�w7��lvnr��{�t���4��0�Nf����������w���J����;#'�[��E�u�6�z�L)�������J&�L��N)�"�r���'(^YTy�V^j�/��_R�i�B)��aX��|U��=���wo�T���f�L�nu�4:k�X}U�C�R�>�b��d���]��Lz�<���A}������*��D�k�sk��.���1���=���J[��%s%��<P�����������o����T��:��V�����p���l��7���H���X����[�H��
w;5W,������*�@F;��&f���8#�tL���Q�.ZI�S0�#�����[e�<G�^b������9z�R��e^j���1���{%�*�o��E���D��EGU�^����8@����{3u�s^���x{F��G��6��
n�j����}�;.{K`9�s+���[Y����V*uh*8z��fr'���_:	����`��:�u���
��Q"_a1�
��A��]�
,�p(�v��L���nL�����P@���c��!�P�U6���
K�����S&�(u`��;G���}���w��q�0���+�wZ���&p�����h�Z�!1��U6�B�0v��%�=n������[������w��S������.,
�w3��8�LlH����	V��0��;[kh�dFzQ��o��
\r$��)xr���������P�%Spu-}���d*D���c
@nv��Y���6@���1���Rss�I8�n�zk��������;�g�e���o�U�k2v����O!�����24u���y]������������N*���q����v,��so2*�nU���B�X��=l�`�����c�	�B���+�6����4H�R�{��V����jJ�,"Z���{GS3o��~�"�6�P>�(3�j��B�zp�i6������#7�.�W��_v};VE�**��>�����U�{r��aK��V2}�&}[%���Sh����GJ��WF^��;�0��1�[�������z�N9�������f���>��^�
6o�p��-��&��b��w��Z������iX��(y�����U�;��V�}U��qO+Gj���R�L��Tn�����7��;j($6��it�X��������h������,^[>�>W2�����*��<x�0�*�U�;T��j����J�=�9����
��Px}�������~��
�����tM�g��`��j�;[j��Bok�(�z���L�������'}���	u�X���b��D�X[a��h:~���U�>wO7�H���T��v�z�n�S�Z��vabf!
��;No�5�G���1��w�
�2���<�6�H��XFv�k���iy`S4���z���t����t6k�_��2z1wjG��E`u�;*R�~�K.�
�����(\C���W�NfL�6Gy��k5'����i���_Nk���J��b]�N��V�:�������IV$�V �z�^�������U�����{�|dA�.:U����Y�o�3Y��V����|P1�3�mj>�T6��naYv�-�@�;^'.o9�NW:��+W�w�����E�d��]�)?g{5���[0�@���B�v��7���X��
�
��5VpJXV:���y�f.��N����kr����E����+��si,�����-C=�CM0S���X>�>S:��CpeA����p��]v�l�Anz��.R��J���+�u��g�����J2\1����c9�cax����g|�)wUv
��3��'�<���)��������9L�����yC�[RzE���*�28Q����n�QJ(
��)#�#c^��zz�����C
���/.	�����Z�]�m�M�w/NLt}JE����V�?]Q����V�
�"��7dU���y�Z�ve�s)e�m�xxc���Q�~��M���<�����
gO{��Fb����"��b�v���Z�k�����Om�����h^D������}���g�r��}X�����wc5�4/.������]r�@�@���;�38�����(W�j��JG�J�B�ph�������B��@G�s%tT����)+����-sa�)���+v�����_VQ��9�����d�F��&j��^�����`�T��91
Fk����;�|U�Cu�5��zn~k��[����!����j�Y������P/���n ��2?�J*.T�Sw+2]�K�*�h������h1X����I{H��/�����N:�H������!�N��L�@�XN\�*���fn��5��.�q���k��}���xn.�����&u7�o5�U��}�]RP[�}��J�t���������0��e�_J��+\$���0amn�h�W����r����=�*��(5�'6M���4��Iu�xn��1c�R:4�QEc��2V����W�&W/zP��
>�GL��������;���KV��en&T�FN��Qu#�&�	���i2���n�:����kw9;3<�(����c��^(�+�m�]VES�i'��A��U��K�1k�[n�E���d�.*�*+w��
*2��7�wO,�T�T��A�fi�
��<���B��,��sI��=��)>6Kw��=���h~}�xa�On**�������{{��%*��������TUN�T��6�������5�w�]��)8���r(�92�Y���6�U��}�'�����Zi�����v2;�^�\h;�=0����R�c��jx������`H;e)���S�bj�T�d���9�+^u�'��6DP����;��J+h���i�������0��EU�ML���\��vB��������/3]\�[N��"��%���=�u�+�M���O_`���g�N��2<�|\]�#��%�(Um�4�
J��:-��0\�P
�5��v�G
�?|c���m�}Q��"_���t���S��%�c��s&_�!Y������6�y��`wn�*�4�w[����(��9�t�`tI��WO�����9�����

�|��v����=]�}/�=��Y��~���
*������~��~���G�i������E�i���b:2��Yeu7���:'W��������U;3���=J�P���.)S\����;��z�
o�[m2jb�t�`:�{YS�U�_y� }��e�u/�����
�WR�C��If�0`��/���b����7=0-H�4eVqy����j����,�sE`���d�|�l�a6��5�-k�It�I�/����^�2����b�Z0;��kN���xH��q�	���*���{�E��G��u�E�+=����n�
uu<�b����r��]^���!�C����E�.�gP�rg���\l��������`���u-�gf���n���P��������L�?k�A}�9��(�&���'�������D���p�?S�su[�n[�U_[�t�?|���sBk�J�G]���EDw��Ezv�u��������aV�yM�&a�w�<���X���b�uMm����;���0U@��n���V������`����[��,���1�B����>���v��I�z�����d,����Q~�~���k��U����nz�1-p��i3]��E].���.���=�JKa�;�@�S2t���9���c���T�89z���.��{	�3Wyuq��>~��-;-�6n��E�����O3 ��Kv{[zkn�:������s/#�T�0�����J�����-��.nj*(=��L
������;De�)�OD_c�i�t`�Z�B���rU}!�f�_A����J��OQKJGS>)$��[S�;�e�z-�7��
�����9�T�{�D�H�
{*�#�qoi�y�;"��@���$���"�kb��U������!�b���h\�OX��
jk5���%�fc{j���-���1x�n�g�������33i�"����s*������}�������v���)�4/0����U'�����V?6z�9/u��V�1^-�]5B��g;iv���ue����(��F�����;��]�;)4�]�x�3���<F�A���A��.����p���9���_��Y;6!!�M�����B
Yc�������e��s�mv�����N��*smuc�Jx��P��_iA����mf��8�-���z��P~��C
��	���&R���1���V�Et�D����W>�����t����zF_o�.b��y�1����}�Jz����^�/y{;�z����T���|�dGL��I1!��cI�������0 ]���P���n����m�����	u�)*A��W��w���/�E��M��}#�mz�=~��*;+{G�q���.����M�i`���6����QU�����F���(*{f�c�l;���E�>y1�~�J��9]`���/U�h�cX��\�V`�����d'�.� ��A��h�=�/( �q�����.�W�`C�N�0�Y����9��
'�;W5\)R��)�]�{sd��Z��F��J����p�n(�Oz���Nb���0��s�^�wm\�Lw�CtT��y16��L���R���7}e�uT�������&���wL0� ���p�FC�����C(<�gZYKF����}}B���[�/���X��fn�����u,����_A~���=���z2��E�p�����j�sx� N_V��/2p�W��j��=��|��)�'U2�dP��8)�^d?9����n2�"�]j����V���5u�+p��/�)�-W�)"4sJ;�o1����K��Z@@z����m�D���bQ��+P�t�����cdP����5����.�GV*��w�r��S3}��`C���~�-��~������8�����\��������Y[^���*�b!�For�Q|�����
�`�<�sXZ�wk���ON5�<�����mm�3�V��XvN�s�i�S�����9;�=�u�����������H���J���r��S��e{����#��MB�=�o"�'�cM>h���t
K�qH�/(N[+��\3�{��r_e���60w���[�������
�M��Gj�75�B�U9}��9w��b9��2h�vGW9tjj��o$���Wk=�vF,��L�H���U���NN�
���,`�mW��G�)>�r�%r����J6��k6�m��Q��_,Jm��sM����I���[�Uxp�0�&@"��V^��0�z.#ia�����Su������_Boh6�����v����CV9';����OuCr-�C32����~�]�d[�;��u��bF�O�3���F/��-X�n8/���PP�Uw��.�9�%ENVs�u&�fa�G6�}��`��Q.��+*�v{a�oJ��B��]��]&����99��@����XW�|D��. ������^����S�'���A������?w�1}_����Q��#�[���
��������!l>>��W������C�_��$������/��	D}{�_���������K����:���
UP���h��*���?�����X��N���X���e"c����-��Ow_[U�Ey����.���N���<�#��""^e��^&%aX;����2,(�,r�M��J�*����OJ��kD�
�"��fw���s�Vy�QTA!]��+w^O��0�
����I/=��kB����
���_��o��Mo0�����E������j�m��q�E�\�pI]go��Z;�l��*Z�E�[�c���
4�T+D$�*��5�UMG�+����(0��9�&���EF7�w-
t��)`UUaA��+"U!QT�No;����Vr�UATI���3����
U�����#*>���H1�2K������0�������*(�S�C@��������$�H���'.8���vF|���U�ZKB���	�
l[-��*���b�2�w�EW���B a�������A���7+}\��E�`y^�G��""(��UM����XaPT}E{���sKaAE��t�s�'���,
�����P��w��V����&o��s7
��%owf�(0�((����bI��o��<�;����
l��{y�W���F�Lv�����\r��R�^��39o-5fL���;�K:T��7�������e�0�u�VY�U+9��Z�>\C��������(�00�zf��'4�n}�T(���c�K^����r���/���$�M��B("$("B�enW�Sk"�FaUq�{����QX�v�����~�q������,,(�(�&|1RI'7���8i���2:�t����V�*8I"���b���cq�7��%r�b���2����uM
���.��g���k���H����|�G��[;�������s��\������*"��0�7���Q(��,
�xye�,DT�+�����@V_^>�l����u$�k�95���C

��[{�"��""�S��z����"�*��������F��u�������,YM��!��a{���$������
��6l����U��,�)9*J�-`X��&��/��Z3-����oG,�;o�Q�kva�3j�B��j�PaQ�}|}����KQPAQX'��9�T(�*�!W\9�6E@�����g����`AE^�~^aP���.N;�b�E�U����}7R*,"�-eE	<��v=�r����p8QZnWH9���:�����Z���U�GAVz\��$�K��K�n�'����cdz]���������eQ���������TAE������"�����8������U�����p�v���!Q�}3��S((��0���[�_<�"�)���w��,
��B��������^z�QQT�g�Q�s�nJyU�{�}vN^�6��N.z<VD�����E�j_��Y��U��xd����b���p[�H�{��NH�����|�m��4`L#Y�m�
Y��UP�0�[��������EHQA���WT�F_g�B�#	q�4�_vha��`J�oi{�w�g���B"��:my�p��\�.����os}�}����E�V/)��P�H���#e�e;H��KU*�:j����v�)� ���0�c�(=�����}N��u��m���gf��,�����uo"�:�����:v�EP�+��@��3�a�u�=w��DQQDA��wsL���TI���_.nQ�#{]��3,#
%�M5Z�W��QD���5��*
�'���cs|g�"��(�"�=��VJ����0)��I���)bo�}�7�C%
��������;�l:�����d�9�H#-KQ��H�o���F�m���1x��3��]�9U��]'��������k����<����	SRv�������;&*��XP����������3;pR"�+]y��s�T0tUa����3~w�����M��"���,!s]��o�taXXF��:g��J�""Wf�hV��T�6eY�+d�u���{Oi�!���1^�N���Z78S
jgM�N�`x�E^�4(���n���U0�E���X�3�������p��8�-�%M�F��j��}N�=��PXjjv�fnT"��(q�w�������s�]���QU��9��O"����saETad����w�0����1�N�r�{n
�(���*,(2fa���iE(�C
�
���0�s�o��x\����4X��k8����r��-5iw$�
��z<��p������N����Lpe�
������F������f2P?Y���Y�����aTV!��Y�zK��
"�
L�����[QP�� �
�n�M{2	`����9���\GVVF���:I�UE�k�|�
.�	AA����"TUF;1��Z�&#�!DGH>\�����t)����]UC(wb���A@��R�7$88o=��J��cD���EX�4M��{������8�a�����U
������w�*�*#�|.�M���!`EF�����"�Ea�a��^����}�a�PTL�N���(An��]��*�
6�3[|�u���*0�,<���
�}���%��i�vm��(r�����8
"��������^����W7s���{��a;D��*+���0�*1o�����W�����gk����9{[���PAaTA'k�<�t�.��R4Cw�[o����.(��"",
0�����iWP�'���i�@�@*�&�2y_w'���c�,*�0��(��g��9�zNUI����wg�g�6�aTC�9��R�����4�c�
U��U�bP)�e�N�S�jwI����fB�
����|+�B�V�Jp��X�.��s-��nS�4��"�"*�M��~���kn	�X$��Y���8~��vOU�T��b������������b���/�1]Gz�q����eq����e���pcUVl�M[��"T���Ei��7�n�^�t��<��X�aEL���^������n�k�W������*���3���\���EaTQU���r�n�DD�wsq+lXHQQT_���NZ�B��[�1�ETQ^tZ�.�j��F6�(�����"�hs-���.����{��c��`��DDQQEFk���\��}F�Z���b2+��T��>T"�#
��f�����y2�=�*{����v{���em�pEaDQI$�B���G�L������*�269���xPFREPE�i"q�]WD�����d�������a!QD��n���v�j���}\u�n���.�����ARATE�n�o�����{��r�=������q�E��Te���tS�`�Q^�x��K����[HQETa�AU�J[���kwA�]�0�s�M���Y�����E�QFj�[xL�9��,��*���W��x~�����:��i���|+:>�����
�\������9}��X�\����-�����A�q��8��*�v*C�V����mR1>��=���e��wqDTXED}W�N���X��(�����v�y�� ����8'}��W�B,(�%w��`�L�����"���N����@�>����E�Qaw�U�h��`FE���8�������c�
��0�
=�����7r�����+�^N�����mn��DQAaAU���/���3��}�S��S��=;<�9���*��������U:�6"Z�=jaY�e��+-�TEFDU�XaB�������g*����o��i�P�DaXDE�v�}���cw���v���z��sz8qw�*$*0� ���V���c5�g^���D*v��;S,+
�0����9��z��u��p�w��Th�
,+
�����|T}���=�z����"vv���XXQUDV����j���.�3���I���QT%	|�I%w�35N�F��5��z�K�m�����
��{�F1/�2�x��z��z�'mKV��V�)s0����yv���w��Tn���q2i�Sx#[j�3B�}B���c�9h���u>�Tl0���[u��r�V�K���w�z�g=��	DQ��|�c��0�\�-bT�{V��
'3�%�T(�(�S���:����PUEUTM��]�u��l�������i��5�_���FPEC��s��}����>�f�j{�9��!���Er���V4v:������2�7��J�
>�
"�(�/+��w�R�.�����>�o���kRl�����1
+
�02BR�;�k, 32�57�������0�
*0�0���p�+z�v����U&��������� � ��0u;����i���ge���-���EaQ�aaUF���O{8�3��g/f�n��y�����FDTTUAY�\�<�y�������
FQ�aAEUG�_5��eB�����1�l&����VR�L��cC��/K���c�~�1|������;�U�3k��+	S*���-:��:�VJUK]����y]S��\�B�QA��s�zv��+�ap(���*"#�^^s���zk��������fw�AJ��*���aEa�m=>�Z���F�Xb��yu����x�
" �ni��EO/���VQEDUDXQ��s�����vox�ow���|�TV!E���n��������7+paJf���P}@DQUQaQUDY��������vd�����;����R�0��(*���0*������K�������K=�s��<�J����/QEXUQEaDQ{�w*�������I6��o�o2������Q@�����o@v�;w�;v�E���Q�QF!^w��n����������������m� ���,(��
�DI�M�[B5��%������
��&O����M����:v����_w}��QFXPXP��{��tm���L�������$����=q��IW�m
��Yp�}�>�P)����U(���\o��>uW��H�
�x;��5:�kk�7��{�X!@j���&�3����3�����A���x���"���+�K�������,9�e��a�\��	��9G�Y�J�DUaQXFQ_��kaPaQ��u]wU�EQ.f�� ��!�eD��������P�������K<��{�i�����Lr�v�>�&y��Nx��aT+�]�����n6'1[E�z�����3��!UAQDo}�1^��2bzs��k��$Va���UP�Lu����#=@�X�������vk�����PE��QQDP*2v�����(��8���e���{:�g4DXDQ�E������2���k�9���si�r��,A!�JC�����re�����<
74��EU�UFA5�7 � \�9.)��'2
C*�
���"�����0��]�p���]��o�aj��,tp)��x�����"�'+��=���;-��m#��9"����Y�%^P��,�V�	{�2/��)��d4q�a[	+�V��p���@�0�����m���!f^f"Y���g�z�6�p��`Qff�k�0�����g��FqQPT���2 �*����wG3JHAUR�'���DTQ�t���F�T`�q�Y����a��
��OM��J���
((�����8U��LM��F�Xmn^�����	BaT���z{�\��.�+�'���e_%��y�UQQa���a�dgN@��.��Q+�|>(P�UKg�������w�,���k7�4vd���Xa�QFQ�q}u��y����,,u)ZO�)EDUUQa{���w����u[f�(7�F�*���
AEW�'T��q�/.}�}�=����k���W{�K�("�������G����[�F #Q�i#y�>�TQETT`QU�g��&�������v��0��	�� UH����q�_{���*O]����5%+���:�
�����S��*u���;;$���1ai���)�|NvR�;-��������i+��&����U��s/��7X�*(+��e��D��"�us���3���b!B��������c�'����`K9���}�B"B��;,�3oXa�����5�P
����Nrvt���XEU�QU�[���r��{��M����f�:���;7������7�FUAF!Q��2���=�v��ad��J������(0�)��<7 #��Y0vX��:q��1�H����"
������9�}���y;]�3����g�^���
*(���A�[����1U���!���0"���
���W�f�8���9��}�5��ngc/6�#>�((��(Q�unu������[#�&�9��aVX�ED�rN�y���x.����1�"���l�9��(B����
B��X�z���$w�;������{�%�31���EX��B
�
�������]�fn��Q6n�!ocT`p��WLt���}��P/kH�;���b�>m��@�-�����:�Z%e]#�F�Qn5��{i����
��>�3��qO�M�or��s�6��*��
*
z��7���QXPTPDc���1�0���z��M{g7&�EA�aV����+�O-aXTQDQXa�_8M]e]g���
�� �"��s��Jor!�a���w�|7.�XQ�UPHaTE�����o�8`fy�*�UU}yq^)��Sf�����2`�7���^fL�X������s�.zv��M;��&�����������+n�R">��.�lFBY�pd	V[a�<Z}�M��c��7r��	yb�����
R�d���_V]���R�sW���+����D�����=0�p�����u�5�����c����Ub���D��E�vI5�35 n��������]{����=�m���3i9�-�K���o�}o:@�3�v����oF	�������3��gp%�M�0t���<^3�!V�f4�����D�/��%�'N���u0�.�ec�V�r5/9Ec��0N=&�;��O�5�7���L'qj��Q��j�6.�5��#@�]T�w	7x�]����!����F��&�3x1:�7q�u��bm��3�28���aZy���������8��x��+k]GC�8
���������
�[����N�1�e�� ��k��c����'�U��r.�����eu�C����=��n�>��14q�����������F3�h|�����1�����h���%h����ZMF�-��p��"i�����Pl�*���|�67Y�5`�}����G;�a�-U�}��t�0����J|�������b�]L���L�6m��K����Uy��*��2k\b9�n�e��T�
�c6!
8�jfd�a ��� X}y��j�<*[eT�������gZ�<[J�%$(��Ept�����;�M$1t[��E�rW]cvT�y;�/%������KS�"b�Z�-��Y��t�H�6�~A�O;GM����7����k�	���;��u�'W�Zf��4\�2��Z`3:�+��S�����}���<]�P�v�cV[�Sn��]dN9�R���a�������&����L��]���<6����E��M:�-�J��w�RU���e���Q
�!���a�9G+��[D��9��0B�?L2�^[0R6���Z1f�D��[�
��E�"�����E����4�
�[����u6��@�yU����]}��.{�3�l��H�V�6�>�u���|r�)��*jE!��&rk�t6|�b�e���X���s���#,��'��������
�2M���J��r�,��k&`8���K+�.��+�YH����FoEe���5���P�Uwb�����f�h(���� 	#�F����@T�q��eGr�1d�������vWgR3T���+�d����o��6��M��P�� 9&j	]���!rl%��q�@M�/����
�-��z5d��������}r�$6�,C��]V(&�1�����&X�LX.�Q��xr5y��.��G�dn�A���F��m"���8��u���V��)|�	����"�u����Mr�W��7�_q$�MA��9.cz��XPx���o��>�;U�4�m��m /@%�}n�A�J^�������wk��+f�P#��	s����t���dWJ�5L���D"Y�si����B�mL�Q1[�U�n����
`P�Y�_TjV�[���l�������j�g'N�r@�6���������L^Y{+��	J �����3�u��=.�����^d��]a�aaH���.��;�4e]G��4+��T�i%�;$y��$5��\����9�!���d���tf��l[�%vW��Y+���/VGw��$�9V;C�m�]6��8�f�t�������T��m�x�[�T��YX�������u�9z�����4;r����A1���##[*i��	ymX|(�@�y����O�]a��-�����rZ���cX�.�<Q��u{�(
�vs���Y������&�
��u��0�e���������u��������+\�
�cq���V��i�[s�Bk�s�����i�Pj�U+K{p��R��w4F��j�����*��'{������-������������l�li�6v���������[�s���m��Q�P�7%E���Y�����+��CZmT���Q�2m$wp�B��!z�����M�V�G���D�����{������	���:��g����C���l��Z�w����%:�e����m�|����� t�x��\�;Y�N����{I\�U����UV]��G4�5|�}����t����3/~�bp��:�;���*L�gr'\|�"�1nn[^s{;���J�*�B�*���6$����2j��i�����Qs��S_
��+�m^���B^)�������4��s �K�-=�>�4��M�.��Y�>7�P�eUx��1���xm��q;6�w}���+�Y3+@�;S��F�f�}�}F���C��x�_=�>���K�n���E>���UfW���=�{��H��cL����*p�����\x���6�w&���+�u�C���v�=�V�7�k�O:�os�%(��2�B%���4���-u%��U�&�M�g.*�Dmu������������9-�5��a������5>�oMWv)������[*vS��_a��zLr\)9�H��pH���T��i����������Q��gh1�S�]�rvec��`s�����<@e^�'��yUW}b7,��Vb8�T�n�B��;D�����z(�M�_���|�i$I@�I������|W?�m��auAq�:4-"�WK�Y[�\v��utw�
nI(`1&@��$�&����j!���
9���v9��������7�'��	����e%"��t&
��10c!0S;�]��f6��R��%\����bn�m�S����o*hblhd&$1��%�1KP����i��v�O������]�ncO�l��
TD�!�Tl0���� d�+1U����p��;� 0vD2:�Q����2	fv����g����LL.I�0�1�����N*���[��[���������^GHJ�
�X�&���(�����lk���E�������������"n��������bS�E�z�^
�blH!� 3*�1 ��w4lD�T���z���+����V�n�L��.�r��P��c��@�T�Z���6�y�qY��r�}5�W��,8�e�����V(T���b��`LL�10�
�ex����v�����F�����j��7)���m1���#G���A+�ZI�����Q-�^���p+���U�����G�#VYNn�����R��C�_<U��`2���\#������^L�%6Wp���%L��]"���=���RDt��I�pX����s"��b1J���:Q��A��kYS�u����������vP�b��zt����[ueZ/A�������wS��7$�W���I�%6#�����w(��$�!d���S.���8B��a(n�u�C]{c������X�ky7l+/�1f�F���{��ZJ��vX���GG��� �]`���Q��r3l��� ^���*��Vfj���g���%_U���4$�NX����l�(�V�:�L����n�\����F��xM�4J-n��PgM4��������\A�T$YGQ���OQ�(�A`��YyC1lm��V&��c�9��M��s��;h����n*�L�MLc���e�`�dM�2���N���ocM;�ZX����
�$L�����]�VMF]!N)-�lI���6�I�H'd�D�U���9�U/�jwp��������c/s3��1�<Q���W��
��!��	��-�&�PK_M���y��T�TUU����)�T:�WJ�1]Bx� �li�	��&��#��FR���P���zy!e3�>�a�����+��2&CgI2&
���m�V��g^k��V���NW�P<��!*r��J�qqyi�����&410@��6l���ov\�/���w��:��V�K|x+&��4��q\&�G��F!�@�$L�cAZ6��V9m����{�aq�4j�h���H���X�,��<���=��&��&4�&��������K�8�5n�����B��M�e��0��j�9��&��6�LC7���X\V��"�T:a��
����PV
�&	��|�gH�t�tWFud�����,��lA�\4��fwgn1��������kF��rM��7�h��,%��S��.��i�>����C���&��m
r9�8���[��,t��9Z�[����gYh�q��U�H���R���w���v:���&EZ�|��*���(����|q)�K��������y3L�W��t]�������/N���
{[$�g>B�����Wk��2����{���K
[��Y�Vt���
�e�:�g8����t�	J�>�=ZT��3)�l�!P�3_���u�m�Ka
�����z���Zy�K�qQ�JV��yZ�f���Sms�9���}���,dJ���\B�>�]�:����D+)�;[�C�����:�77oI�����k�Y@����J]mgv�j��jWY��	��o�q�u�V3O+�g(G�0���X�H���A@�!�i��k��8�Y&���U�$����6o�N���TE��Umz'�s"�� �	������=q��y�{�d���\���I�������|,����$U�K�IQH�!�bl����-�FE���\8��t���������Q�q�y�s��^�-e�6!�cld&6efal�8�S�Ob�/L�]��;x���4��6�X$�*�-����xE��C��B@���0�'������Wq��}��='����H<8D�2�����9��B�	$J	�b`�!���F9�������������5�:��ze
A�r���,&�Nz�Vy @1	�����,�3��x��iqu�~V�R�*���a7�1���<@l����e��*��}�������}{�i�mem4-��HB�
�����k�1�@	�6&
`�5)V�����=/9�/���[�`����z�k�Q�W.�}���0LHL@!�	������k�V1��"c2��������=����g��������a�J
�`�Pt��i�oU��M9V���]W�;��'j:�f��O��>���eGM�=�uP�tv��q�{w
u�F���
�=�j����o���A���DJY���ayNf,f:�N>WYc���]��tyf=��1Hbn�=���^����Za+}��B8k���v/Dm�n��;���\��u��V��3��_6-
�S�����Sc�9�s�K��m`h�
]�l�,�	DFx�7TE����� �zK��n��q���N��rO���U��"
u�M�.�m��c���H6���kK��YR���U�����/E�����uu���JHz^DXo�30�Hos���z]����Z/���
��B�TB|4[�nt�����\z�=&�[&7&����
��i]\�����b��z�D	0@�4c$�$����;�y��K=c-��t^�q���6�.R
����&�|����lB`�HZ6��N�b#*���g*���g�U3�Q�yG��8�P[�1�L!10L!1��T�e����������h���{}��q[�E������&�2C�����&	�61������wY�gT�;��),��fLQ�8��nz_��R$��$���:Wy��&Cc�c`�U%>L�Wm��qY��WM�j�	�c��}��J'd�u(93s0��QC�$��� /3
+�2oEb��_8�����v/{�%}��g����
�b�F`0`	�����{^������O��(��v@[�
�I�iqFF-T�T/,������0I���H!@6Ay�]�6�3P���P7����%���4�f&�T��]O��ch&��
�0�9��0@�Lm��LH6���,�X���Y,>y3o���x�������Y�T��� �F��+%0�Q*����H	��i��Q��]����ke�F�ed��[l�����R�er��l���NP�]��Ut_F3�|6<�
�<����}Z��}�������n��c�:�`�$�o/E�T�*��`��w�W]k���`�!��G��Z�^��\�9���0\�6^�{����QWOi5��{9

��B���aI���3����k��)�C_&�u���������VX����$3���S��]����D���5�&w>{"��2�<�,XIu��|�����mbj�A��#�|���7w9��Q�����fi�*e��V`HY^E%*���' 9R�|_V���x1Zh���ZGK�RK�6��� Z��KX;��h����Pock��UoM����Y����U�<�l�3�[Y�e���1�n���������qa�'���w[{#���CC������u��eb���-��:�����T&�UT�5����C
b�2T�Y����E�y�W+7���6�����S��3GvTT2�)����d��KR�`e���<���!0LI�f�����n�G�~�oY�a�
�����K�S��s�������/{��l�@�@����{�v���[���|���n����v�-C� ��Q�A�%����� @���!a	��w���E���3;d�-���F�I�c�n�f�wV���.�@c� jI������`����[���,�W��z��s7A#�r���e�Y{V�����U�i���lc��6T�k��������))�/����-��$&���kA�b&��rS.���bl�
0���KMap�]��������MY\���Pe+����M���d0h��	�
��*��{��0���o�P9\k�tw�a�r=�'�x�"���d���� @Y�c���%�����IS��r���^�
�T�fSz;.��Vd�~����0`4��&,�6*�EdC�j"��\w[C/����S���u��F�����>��Y�23zh��5WG�[����n�u�2D��:Wp��)w_��#���:�m�C0�9t%��4|�D3dTU��Eq���,��<��A�c��
Z��!���mBno_a�r����uZZ�k��%j����PEZ����Y���Z%h���)Xk�qp�J<�;r�/%�X���m�r�AJ_v@�qR'���Uyo��-\p���#*#�jK�9[�j��������xz]V>�������o8�ww�o&��:5v�d����I�NV]3n�������2f�nwo7+���
f	t��h������jAuTVP�D�
��vm��|�
]�f�k}�7�`�{�7�C+��k&mffR��+23�aKr7����E<����}�A3��2�x��e�s�)+*n��
�e�W��1�l `�&��*
�D�������x����z�>x8o<�;��r�����1��PEU$�@�)&��$oK)����qZ����*��5�jw��Z����"��;��ShIY�%0L ��&�d��������{`U��
����cu0M��HK��z�	����e��nP��xcL��3l�����ND�+���w3W�	�k/b1D1���q��4d�,��@���	�6����xR��>������u�L%�V�x��\�C��Y^�lm��`��T_���w>�
E�S[u�{�����1^���M����Jk�)\�W�U7������ h���u\$�VuEiN7g����;s��3����T���S�Dl2]0b`�d6Bd��,��i��o:S�M�/F�-T�B������ARj�5�����	)$�
I10��1�O���T�/]����o4���^�z
�5�{OeM�h��J})�4��0l!��� r�X)� T��"��������pP��l��}�qb�����A�nH�#�
}V�y���i<m�g!�68��Ed��&#53n�f&n��D������&c(
��3+��W����`7��ifK"��������vY����;
�U�us�v7��������z�`����"��7
'��vXH��y�%��9��u��	���o=A�����K�������n�H��@����k���������S���[O�N\K�u2�YtTv;j���X6I�-��G�T��<Dy�7w9v��W>�n=��t�][jc�*����b������Y��]���?N�M�0�U������K$����n2�Zn�F���]��h��
llhs�����,���������>��o�6�#��4(������G&����A�F3J���]��=\IkL�����Bc4%w,�N�9���qF�q�d�NOb��DyW��0`�
���@bLTd���q������\�����w�u]�=AM:��W�&[������
�l'����0�q1����v�%n�@�`������������-Yle��{���M�0`R��������%���e�>�<�$��\�.�=hnJ�[I3V`���V�S1I"
�d01��dz�s��=[t3����\�oG�<�����e1��U�x�����Jc�l!0@�`B�&$d�Y��e=�����<�T��8L��C��M���4��
n�)����������4��0���i=��8�K��Y��{!���I}��:��c$�����T	��S��`�0L
xL{�K=�i�=�vNX���c��/����f7Y��P�L�^D�����]@����Ah��@�0��`�����UC�[��{"eA�����&.������&	�0@�$�2 8�6wz��0^����tgGGf���q�j;��5������~3"�`��,���l	����S9�]]�`�46,���B9�/�]+��&��6��X�v���Y�4��J+���p[S����:��r�n3.�9x4#:������r�u�h�5�F����.�m��l����X�;����4��E����-�V^�1�T���D��� ;���hvMow�[�N ��;l�H(����"���Gjt��Q[�OI����K��
V������ah:+N�{o�H��s����&���!487����/E�������^�_,����hz�rvd���O��Ym����2�;�=�r�Vj�V�K8�C�%����WX������Y��������Z���.�kP\�cNn �cQ5��1�����*��Ox�qVp�.c
d,������[���;5h��.P���+1*$C���v,:�yUv5�����K�DDL�B�`KL c`�d0�/:f�Nu��,�G{���ky�od��[Q	���b�t������(c�2�L����E������W���������L'�|�#��a���R��s=��I$IE$ $
��b��}������uM���\�2;���������ZjW�U��Uw���I�� 	�)"������`�=�EZ�����U��_Wb}����}{�S��U���E^C`2C`�h!�L��������Y�1p���E�l�t�6I+�h��u��&�B��r�:�ghE�H��b` h�}6y�bEdc�����p�w�{�O-I�0iv��z�T��M���!��b�x�mV�8�b���rU��%�\F�����7o�I�6�5F�m	a%R)0L	�0G�+������!�z.|Ge���Lk�o]Q���9g�D\:�`�$�L����0�5x���uX��Jsfe9G���V�W�p�=���Gyv��R%*�
��0�+,�S�N����5`�Q���r����kS�]Sk,�df����#�g�t�`����\�,Q�.�\��lq���.���5��s�!7R7�������j���t&�Wh\czc�w���]A�����@����J�]����z�F�
�X�4��j�b�]��](s�1T��f�U��Y�\/����ZxkE���d�E:�i�R�Q4*ph��z��v^4�R6@���e^r|I��w)�&��+���a���"M4��+����Nw&zV��l�t�%����9X�,R:7�u�#0y�OsrS�gu��`���lut-_���f�$���=l<���M��s�u���R�R�w���v��d'�Zf
�q�u���UC;3������<T�B�%������v����-;�x��F����������9W�=���[��D�	�*��I�vQ6;��39N�1P�r���X�����C ca	2�$s��=�I��gyQ�}1��I�WF�"��:������[�
�a������!���as��c4�����cZ��cF��E�7m1�m�cl@`�~��	���y�|'h������rnk}b�4��mB���+s66��k6���HQB�m�`66����Ywy���'}G&��X;��t�T��SpG���&r�c d��li���/j���fv�z�{:u9��C�t�}����+���{�4�:	H�H�lI��6xQ9���M
�_�+����n�Q[�6�,@Uw^����e�H�V{��Zd6@�0L��l���;K��n0o����#&�L��:�,���sPB����B��r���j�&�L��0Bfzf�9*8�����e,�hpe��o����b��	�jv+1��T*������
�blI�c��]�����
��W
5��lYzU�*��_����%U���A�����`6
��v���|�,L���$3�]�'b*����0��T�K��s��E������xM.��C�<l.�C��]j�C$���:u�����[�������u>q�.���������w��;�e�P�1|�r��s���u��eV_�y�3�:WR�����#��\ �C{b�0��Z&�:=O52fw.���W���Y������e��5���|$�����vmo;������pKy�=�s���m��<���_B���9�dsmu6����:�T������5�q�+�����8�v+������6�J$��)��s����b� j�9m�7<�h]9�XR������=����Ahh��a[�����>}[],vd���B7�-��:�d��whf���g{!�:
�`��Y���D��>�5s�Q�w�7�D�����Aj�[���sE�L�Oe�N���,�\��o�%'SR�����0 b6jj<K��}�����;�y�w�kQ�P����w!G�eY��.���i��LI���d�2���>���}���sz�]v�Y+���j�U7�Nd��j51J�cLc��@�d�L����0"��3���wk���-dX��Uu��&}s2W�r�
�6U$�D�9�bQ�����o{B+�
���f�c���d_M���j
)&6	�@Lc�W1ke������Np6%����!��:{�������yWp��� ���HJH�EkT9��W}��3}w���y���br���mJ�p����5���r�bL�b`�cL���"o*���T�o[����	-P���)��'�R��������Y��b�&�UE5U�]0�^�������jL��r�����E�*���Q}Z�8t$���LC��cg�5
��m�lL[�v_`�Hnv#n,�����^����\V"/1��u	� m�HQEID���N��O��p�������{ot�P�v<}�zm^���9wGz�����S9x�s1X/5�w&s/�����������* ��:]��gZ�PQQ_�����^Vrp�M"���"��i����3�D!DQ��{������R��,#�8���;*�(�'6���W���AQa����j��Yg�A���%������n���}C�'}&�"������=}�
��{���N�_�[��������E��O�����fS;�}=���AUU�������(��������~�����
Nc��9"�"�,���2w:N��"(���}��fw�A��N�{�i�����0�YQY����$"�����vZ�6dU��aTPRaR����}�����a���Q[�q ���^D�s�5-�\�c�����<<�u��1EH��������Y�5�6N���:Q�o��3M����T9��D
/r�(v5
�ii<���)?g��aAJZ����"�&��7�w�q��QaHk~�Mvw�aEG>�������r���)
(�o���5�UXY���W�����0��+�%u�N�]	��1)G,
-���S��]\�"������U����������[���u5�L�nHY�s���^��9��.pL���c}��,���l���g����~�s���J��}���TTUb~��+s��ssh����l��6�("��*V���~��@�0�;���'�U<���XaD��[]����6�aQAA;���o1�aVU��/��S��0���rr�eU��o���R���P�W�^��v���7���`�0�i�h9�N �j�f���wV-�����}d="��R�|�8>Wu����|�{����q��<P�oO|��woJtg������Po����J���QU=���V�Z*����w����i���`�$(�.�����s�����a�aE�N}�l*��(�/�z__	���EPUV	���``�'�����
�����2��������m�x�G;���--������-O[�^������.w��U$Hw�'���:a����bZmn����q���o�������8���e`�
�Uy�.����x��}P|��}6is��$DXK�n�DDE�>�9�����+"�u����W�Jr���(*%��}�s��UT���{_}��|�xESn�=��k�V��,X'��W�)�*��x���y�'��e����V![��I.�^�����$o���zt��2�"���:T-sV�v�+���^6���oSl:2:�����q�)�����;1���pf����r{\�������.w��0�,(���M��W��Q����g?_9����FXaDas����w���
��'�U-�����
���/�Y�������p���"���"�O��/Taaf��1��U��_:����	`�����7�v�X�T�y��fN���������h�����r�h�P�FE��6*���"������DoO���7����'�C@��7��2{��r�����=��aB~�gx}��
�
�9���}���
��+���_���1�`a�����{�" ��W��2�;�QXQaE3[�_O��w�"��s�;�e�^�EPHQE��k�}��h�PPT��������|���5�ot�PV�5��$��AP3��������g}CEb�����j��)
�M��v%:��,br�����M�]eS�g����8s�����VK����/���g��"�0#
���6�����8#$++����{��N�,0��"�Y�M��H��eUQQc��=���r�PPa����}��DqFXA���^d������}.���F�^�9�*�7e8v8x[�Ou��y�4Q�|������'{���E��y����U�7�8��{���[wk�Z�����y�yX�T�z��q�����/r+H��v����	/\�(��Z�F�DX��:�f���������L]��]�(*�����g�s���++�����yL�0� �������{S��U�U6��s������QUaG�+PYt�Y6�;���S9�^\���+Gh�����U�c����3o;��J����E`��{�
�.���qr^)�f^�R�#s�/���XG���9�]�Z*��ph���������33�EQ��=���QQ`ATF���'/zLb��"L�93���TJ������Nl�����w�]!������ID$�t����4�TQEU����=�s� %���\��"29�uB:���>f;8���k���xO���zz�|�}�j7ju�R��Y�N�u[����������O��b	o���2�$�t�I;�R`B�Xk��k\��@JjhB��ok�}�z�
UXD������d�UU�V3+~�����(�"
���.r�c!UF!��3��r�DQ�n�s=����L�
�(��K=3ye*0��?W=9����}r	G�}
����U��F���;;����%
�R�l)c�O2)�����v���Ci�RwN��D)��
1�|sU-�z�����`!8FK<+�*-����w3
��������
�}�3����QADFW�s)`�F�]����rFUQQF�b�8��,C�e�����0�0�R�[�(�aV��EQ���K+��]����s���U��vro����,u%L�T�b�v�E�\��������-��
�+��%������.��y2������zK]x�APE�!�Vn��/{[w=���w��+;��t�$����"�(��B�)�y~�Iw3����S���xs��9�t�/"TE!�UAG��W�H�Hv.�8f��U+�P��+��p�(��,,"���;��W}����o��%zW%�9�!����E�XE�=o��K���^g����gr�xKKH������
����;G��{��d�s����.���r�g9a*����Kf�c��|�<��o�O��u��K�XU�XQQ@aU�2�y��7�x���]|��7q
;]�4R�v����r��:�!>
CZ�K
�}����UB��y�^�w�1�����4��/��i��y�=��o�-y�����U��i�����-V��z���U�!�����sBD`+����""���.������;�{4��
s���9$��12R������g���u��K�#������aTaU���g�J
��M72k*�����*�"/��T������<��;����:
�B�,
����������7�u�=���\�]���	��������1C7���E�S!o`�,M�'���"������ �3��^}&�Rgr���i��i!c��}CE��2EVaE+Nv�L�8!��RLhv�t{����U��
����AEXw�/}���{���[<��Iw������^��^1TUXDQ]����N�3����w���O/sk�9��\`���("�
.#�7s����G�j3N��j�
P�AX�ua���g���jO����k� �(*���((���3n��e��/�2��U���J+/���;%����{~wW�x*��M�no�������1��%�t���#J��x�3[��s�2�W��a�����w0�k��>���C�(P>��i����w�a�X~7�>�������/�G2y��"������s;�Pa�A�_v������XX�!Z~���cTTa�Q�����
&0 �*'���������V�vPaU�UBy���E:�\`cR��wZF���T����|>�T1��E��{q���49�w�{K���-`EVER���O-���@��qkX�"�5�PB
�"��f��;u���44s���}9y��`E�TaE9���2������n�������;BrTXATEG����VZrd�T��Yvg��3��9��+
��3Ei����f�:��>�����"�+���{W���;9�m�����{\��Swy��"���"#
��(
����$���'2��}�����7u�����,*���(
�e�n�1u0Y�sEJ��n9��SL_�:��������J��$��A��oTKe,3����4M��~O
��W��z���:��)I(����%��	a����3��EN_�9s�}���_(������ss���HUX`HL�V'/e�R�?W��r��5����Vq��<������)�g�jg
�`PTUF��7��"
����EbbvdR^Vr����]0��"��(��P�|ON�1,G#�,D����t;�_dEXD�V#���~�s������6��y������(��������0"�* �����;�nzfd�&��s�<��+��3�I��y�FXEDUY���y����>��y�����&���D"������,#��W�t&���]��;�y�[����K��rXEEa�DUR�����w.�&g�E���r�w�l��JxE�TXUG��y��D��.4�&��Z�f����FXEEV7�n���.�v�y�{=9~�=����2W��iE�RD�E'�����v��}r��W=r�t��u����!QQ��}�#"TpT���j��N���^li�;�WU��t�b���=��<D��.r��=~X��n����sj��%��=�r���m�{��!��u�.�r���l�����f�����*(� ������{^�0(�	~���NNUvn��ATJ���w���l0�-���_W��T���V|� ��	3�~�}�g�A��Xi��s�����aF"��{Y����`Q�aFw���k&_*aQT|\��vj�W7-����������{>���EAEaFA������g�+8�C��r��I1wWr�J,QUQb�
�^�����!����V����ht����*�"�m����:����7����J�*�����00�
���sf����v�O�'n��=eKS�����b������U�;K�v�bgnE��w���tP2�����TQEQ/7}���k^�el���3�����c;#��(����,"�8�'f��/�{&���;������'(�t���"��"�$*����{7�8^��N���+�wmZ.�Azu(:��a�!X�>�rs8l���I�q
�`g#�^���G��N����t+9��a����r�wtZ~�:��*��_F��q/����"�/
��������s��oUc���gUa�H~�7~y��A�c��������Ua�E/���(��(�>�p�_����TEa������KPEDE`����'7���+� �����]�2yS�*���*��
����q���W'�;�=uN�%��L��B�����0�
�T�]�������z�T�<�P����
���L����^Wo�;W��������v����"�*��" �� %v}���y��x�l�{rx_g�����t�s9�s��� ����(���(]s��r{�y���������;���Rsw�9��l*
��(0� ���0����=<�s�Tf�:V{1������* �����wC������s�>���0������"0�@U}�������(�[���_I���mC��]��sc!�FXEU�Vcc2��3kCn��pM�l�l����v��QaEW�w2���5��C��(�D��c7�����#.�f�]yS���I�5�{�,�3��� ��n�w	�^�����1�w�"��_���ns
]�E?�G��:��m;\������*���������T>�Nw���w�zaH�K�.�~���**�5��{Y���"�"���|_���g��
���in��@��0�=���(��������]����`�������Y3_u���e�{��oj��c��sr���d�"0���sS�%��v�f�;5o3��������DaPa��v�^�r�`����H3��7i�Se?�A�Fz�����K���z�=rT��k\�g����+���8�"���>���P�M��aEK�^�<_}��[yI���`AE�aak��Fp;�TV�����3p{��������
���,(�,�/���O��y�����T�c=��"�U TX�SW����{������
�o�����"*��*��C�L	Kt��-�;n��
�>@�(�""0��jv��:�����w6Gk��l���:�A��F��:���������U��j�=���Mz
��%%�[��i�^[3P�9A�.��
Z4V3������F�ew"g��)E�������pV"���w9�*��0���\������aDQ���K�o��r�!}��~�U�UFE71�0�pU�y�_8}����#�����1
�L�zk��wfr��F�2���,�"�$*�����}\�\��y�������fxy��8���*�"���0�������G��s�s7�y����k��`EUQA�&�y�+�|{*�Z9��:���I������
���"&�2�-�5[��+�����������QVU���K��o`�GvGj���-�X�j0�����"+�)��[����=��w]�UK\+���aFXA��a�N�o����z�������o���OWp�C
(���*����=�������q��;����wa�
��*(��#��;���*��O4Gd�G
XUqj
wv5����EDEQET�W�^���s�u��R��Run��g�|H����@-�E�w�>�k4va����;#\�����
��RE�g<^�� ���}��6<��J�5�'�5�����vv���!aE�a�DAP�_�����(�0���
��s���;����(0���*,@�x����>��TXHQEUXG�|�~��E`X���n���TE��XXa�a�����~��b��
�0�0�������RW�}�\��*�*��0�0�����+�M��\����*5'��;v�<�0W'����jN�kr�?�L�e�{�����u>��G0����Uw#��\��X)r����Y�������t���t���4���u��e�T��^��������y�_��k�C|���
j���7��
H����Gl���5��2���ZF�X����{K*U�m�G�vX-.Y��
���-�sJ�`F>��Vt����vtz��������W$}"�lc�v������\�Q{��m�Bz�����U����"�FH���Y����r��
��C1�]��M��s��j��+S;-��2�\��kt�r���+�%�D����hCh�f��a����G�X�y;�1oAU6��tRW��wRh=�,N��\��;���nWUN���8ZY�>)fy��H��)Q]e[�4+%ZS�wT��.��.N9g��������&����,WE����s����>�{*�G�T��8��t���'fn+<J	'��IL+W@8��[�9��eF���^�3��WMc�]8Sv��xnN�mvur�)v���(p�Rj�"{�wYg={�����������TnL��7M�)����;��p;�\atmV����%�XF&3&�p���Q=\���U�iZ��mWNBG�i������yM�M��\�Is��T����Kh�_	[A�X�`����FJ��`p�N��Kt��x�R��,���)�kl�kW*$/{X�J������4l+j�stx+�����UD���R4�4�fsC�,���nMd�u���.��u��X[����;C;7�W�����s����iT������Q��^�6�wLi��v�Vq��%���k�/ehY�u��-��v�g}6�.V�Hl31c#8]�"u�����������cwrM�
�I6����-k����9���k�Kh�Da�o�yF�1��fT�3p���VLb���{c�+S7����u�m�U��R9�N����>�@>��x����(Mj����r�5�J}���v�cV�vd�����]���w{p.kf��)��W��v�8�����WJ�j1��4]�s;������G�pHq�#�������*oj���	���*�,l�M5�.��u��������y�?<;pf��NKU1����R�IJDpm�^�F:�3I������Ep�-847W�N��@�8��z#�R�8�+N���7��n�����Q�B�f[��+G�5�SD��|��4(�����tV�SRr�tS���3����4fr��i3&�T`���wa���2f%���w������l��}�|+���4��r<����t����o�gF&
=���A�r��7!7���:JW�*�E�uV�v���u���fq���"��s�-��k��A�Q{�����N���ufu9]I�A�u��0���h�t}��(��n�[Fu�e���r�����]`|�n\�c���c<�:�#����c��!�[��S.�q�I�U�i�f��%�'uj�&;��wm����6��=sr�=�%��.=��Sw,%0gu�"w�Q�Z����{f3�M[
3W��
���^��%�d���;0������ltugu���5�:g2�|Y����R9��L���sU��]@mVd���E��Ly�� �nZB!|��PHU�HXK�)�ty4��K���l$���:8����O�����1�;+�L�������1�k=$�j��m��-��H
�fk�I5������kO�s0yF�i��n6�u�KU%wB��U�R�(4�f0�� ���F[0�6:�B���mb~�%�}Q��%�y�7^���t�T7N:,����@F�&^R��Y���u�}�ls�d�_o-m�������x�R���k�S
$|����T����wy2�te.<�`�[��sX��KF�GP���C�:�GvM1|~�����������c5���Zh��K`�N�h��~���	��g������9>G�_�4z�N�cw]�<��a��WPt5\�	v�}BgT��W��VG���Y�#vWC{���n-Me��$��s������F��M�Dk��7=5��/��:	����D{r�l�S3�cR���FnNMM����P/�x�����������l�0B������)W�\JL����i,Do&-�b��������MQ3"�`�<�`����s����9�Yo��9������O�G�A
Du/�LD��E��I[����!������H�S��93���������g�0GZb�����Py���q�L>�*@����g�Dbc$��B_&!O�O$ET�E��B9�"< y�$?U{f�G��>N��El��H>`��h����cI�}Q�T{����@9si��:���lS�2��H���
z�Dc�c�1�&�X'���0�GP#� �M]�E�!E�./��|�2Djb	�?3��)����Q��m� ���Au�A���#��_7������Ls�[�AI�@�H���nMJ��e>`{$�2��DN�J@�����c������"#�I@��=V������j�H� � �9$G��))� ����%�`�bj`�0GO��
�"""8�>y�]�9�^���)��`�vb"�H�H0@���Ol��\����y��$@�R@y�"$D����v
���w��yS(���hO9���Z�!�b�:����n�Ai�e[.�q�^�\�9f^2��r�i���G:��+�-�`�.�U�i�p-�a��r��FCkC��L��CtL6�5����^� ��[�t-�M��e�[��,���`��k���\���#���mDNr�/p������������Y6�@[��Y�����Abso"���=�+��~���}��/�3���wvgw�l��>��e�	S���yA�s.���������r�����P"h�]U���{�S��G�+��J)H.U���� ��$7�P��qY�5�f�:�$/\�$����_�:������lz~�V�g���x����b�����f�yv����T�Cj����L�uT������8B��6CmX$7V����U�
>�'�u}��X�R@CO3z�����hk�,=��r���9�_Y��!�U
Cw*�Cu�o���mY)kY���R�&=Q����?������3K�(���}��7?bR����d��5���\=�/���!��Hn�\!���H:���V������A��V$7��c�����^�<2K�hV���P��C�;7�S�k�W��|�{9-��~zC�p��VR�]��PHf��a�Xa
�\!�jN�f��c��:����?}Hw^ZY;�bZz������l[�A�����i.��Y��=�/�{��HV��k���yU
Cj�p����ja��p�n+��zY>���>��(�=����_yV����k=�_}�l�#�q�V���=�8s|���9�_z�B�PdT�B�����n��-��AV���V��VB����;����<�O���N�3�m��6!�X������e��OX�]je�������%��Y0��^�5aH<������n��$1VD��X!�U+k����~_���R��L�9����~�W�������
������~	���K�/������VR���m�D���a]Y�5��+�&9d�����q������l���!s���#��k���y!�{�9�����3.�}���~���@���8C8�!���!\ViqX ���U`�v���*�A}���o���s��z�R#�#�DMH��H�-=�55�7��w���H�C�!��<$�<���I{��^y�]����$�s$
`'���&w��I��H�1�b"9rw7l=�(K����ALDEMTu�-���T���$x�� `���
{�PK��<�;S��'�_I�����E1d�G����1��u>�C*c�(G��P#��������uY��N\���-��Dcy��DD��"|���Y�������y �� �d���q��R����VHGq�d��96�3���s?O����L~���E��J����-���&�v����B���b#�����D0�5�<�����N<G�k�<�AHi��I�@T���~H�w�J@�"#X�����L@y2��A�k�����A���A�8��117f����K�����"\H�`;�PDoyPm}\MO������\}2u�b#rH�����1"�H�b����o75�c���"#P��H��"""��B�`��[�b>"2�"-���E$�g�Zq�!�|��X���)"#�*}��N4��U���g� w�t����^�7�������r2����J��S�yA����z���U��Op�l��V����������,�t�C��^sY���^�aYs��6�{���,����v���������>��bvD���8����� E>����s��X�Xr]�����_vl�a�m!�
��4�����{��#��i�KV�k3/!�jx6*fd�{�~�zy�)������F��0�|�V������o�w����ZC5�!���!�*��5ufm�!�+���������c=����c���n=�5���A��?���.�5��\�������q�����dWVBAM��A5ui��&���'$6�,��d�7r�0�:M��K`�o$�������Un; ��}{g��8j�j��+��kC��}��|<~|�C8��CuX��*�����!WVB����U4�7P�5��>���K���c9����SE�����z����I����
Q`�zZ�^���7�7����k9 �R$5�@�6�R�V�nU�!�R$3��)]P����������h�Y�������t;�]_�<��(��U����z�y�;y�_$.�	
����a�/�R*��3�T
An��An�a�ZC}w��S��e��I����V)����	$��������v��*����6�q�K���su@��I���i�����H�Z��3]P��`��*���+dR��J���
��O��YBJn�,L��������Z\���@��%q��*��6�Y0���i���r�)��H;�f�[�!�PH��{�������<l���������]I���
����~�l��r9�6��[Z�n�gW&���I��U��
�VR�m�H�h��7��4wiQ���_G�<����7��&{H[Ao�A@\�S����������Uad+VD��T����-���7��He��A��$�g����B�����7���gy�lv����&��T�u�Q���Jmn�������|�z�!\VM!y���yj���*i��a
�����A�9�A7yy@�M��-��QI�1��B8��	�w=W�D��'�#��"+��1_v�bp��d���~�(���b"��>H^5�������0o���$y���?!}�:����A�r�
H��iDG~����>��i/>��[��]@��� 8�u�
~H������zOT��,� ��O+�4�[�<��=��GnB)�)�X���<�zj������b]�H��":��T��DC��z�>���k���[L@}� ��������\���Nu�=�">�0�Sq��=SM$���=����2�}���$�>���0VI]�N1#�P��H>��H��.�DDu�o�����u�������=�")�
b#�AR �1�R�u��3&��X�G=1P�H��C���3���G�2���D=��`����r~�c�}\c/*8��������"��� :�$Wf�����"7&���b"}�\�A��D�o�R1�0u���8�A�L��� ����"#91�������u���Ai�HA�F0GQ`��A���`���Lh�{��Dg�I�z��DA�� �j��Z�e]=+g�f��''d�x�WEE�����W.��a�g�a��������Pst]���7�cE3�&]��P����>�{��qviB9;v�!b�Ju>�����El�i��!�_Rp���ef;q4/���#�W�0m�o���5Vu��h��G]X�G>���a���
��HV�81V��;��;����ne��������;�4%�2��x�V3��.I�1�o�#t�r�;lZYTw��(#WC���:u�-_����7.�1��j�p�,�����|��)V�p���p����7T�H5X$.�d����5uI�j!�����r��[���~�V��$�P���L�v]p?}��aE�����\��gE^v\���P�4��|D���-���
Z��8�i��d.�
���[V$:�;�3��}�tY���GEb�F��"B������5�������������?<��1Xi
�Xp��VM!uj!y�JC��!�U��[�A����_ul?w�|f~��o&u��c7�����?�/j�hd����CG��c���!���j�!��B��*M�mQ���2��!U��������5~����->����/���^��(� ��F~=u?�_|���A!�U�Ar�l�j�����y����j����A��>��v`=�F���:�vek��sw���������NY�X`����-�3Z�/Q����}���������B�Z���
 ��8C.U�B�P�
�T>�����$��{�����k�{t7��Yw���,�M�'�����P�?f�����j�g���|".O����B��!��B��U!��dU!��!�U%!����9{��2��R��6�����������B���;}��i�9����~���YU
j�3���3��,�rt��tL ����RY
���*�$/<��p��r�}���n�WD���=���Pu�Z�2��>��uz����7��'$2�������Cuk�]P�
U�CnU4���!����W������}O�o�uV��c�5���jg(�?��6�=U�y:�w~W���TA9��fffn�N��d��T���T
 ����R2�$9xp<��s'���p%�����d!�$��d��t����������y"- ���"����/�g'�q�7f"%�� 8��r}D�Nr�^3�T����5��H��}A�l�>�5��������u�8�B�!�z�rIi���o(��O$�\@$�)�<����0�(�$L�)T��1��� ^L���={���=1w����"�'�����x�s�T|�� �rH�H��H�|�O��vC� evf"1`�1�)���$"C��	_I,{�
�58��
���` �"S��}�G;~��C�������Du�j[��Gf5����^�$$G�1"��0DDG�Ok�S&:�{�IC���������DD����'���$���w1���#��b":�������~�����3�M�P��#@	`8��Du�������zN��u�}��#�y��1�b#>� Lm�j����4{d<�{&���0FvH������WhW$�I�HA��f#���"R#�U�S!�_1�:�"s�rC�y"B��U��G��}G,��A.���s���
�v�Omqd����0v�q���WW+b���7&��a�K�j�6��j�gj�%��o��*���^7]O:w=v��
>�m	��U�8��p;����n�Q�(�W)1��J&��%�2���g��eol�Q�2��]2��A�?�0G�W���l���f���U���z@�9���������c4������"���:��y�c]�f2O�ofzY�c��A9���'F%p��3�V����C7�H:���� �T�A�T4�qY�5��n�X�����1����J������B�zp�}g5)�w�>hys��JS��.����|���>{��d2�
������UjNH6����V��jO���G��<�������-g�}���9���w�L��,��v[����y��m=3�;��7����C5X�WVa
������ua�2�Ra
�Y�
TZ���y���?g����o9��n`����z��s��?g�~��8���e��*���~w�������p�Z���M!nU4�qT����,���d.�������"�!����X?xK�9��N���!@��$�o���K����>����uv����c�^�wo���mI�$p}���n����j!y��!���!����uf}�y���~��Ll�[/{t_��R����
!����y���I��)��o9��[���!Z���T�C�C-���U��r� ��)�Y)
p�Y_Sel������v%����s,�0wZ���.B/mO�?;��}%�;��b�T��RR��1Z�HeVD�j�rC�!��CHb�Al������j�QF��������P{�.V$�m$�nY����6�;��9���!�U
���B�A!�ufw�Ar�4��j�����B���������G7g��o����l���������~�.�q��*`8�w����{�~��j�A��
C5�&\�&�Z�rB�`��uC;�f��������<����������|�P�//�e�Y
N�^]�j���wO�}����d���d5��U�p�sj�-ZAqP�6���t���dA��d8�����W��
@^� L~w��c�/�?}!71M}�����hAhG�^HDq�)�#������^��Er�d$���Q|� �")����������Gf"A��AT�
����H�=��W%q��M	y�ly�DB:����	��M�c��o�{����D}���F���#~��F&zN0�<_oK���P����X�DK�I���<��!��LLDS��Bd�����b6����-=�QI�
 s�������_�"'�����D|�k����"X&�5�����z� "U�% %���#��q%��L�q�<��k��D$����g��M��b"��#����s�D����_�Go���rzve���Al@RD|�����0DDG�����0���;������6�1�{9r@J���=y��c^���(A���>H�vB ���Ih}�Bb�y�b�!�(� �-3&:���U$F=����L}�Q���I$�DD�!�Ds�DJ@��@@}2���h��ixG��"-� �""�%�?*RE�~O'����[E���#�������{��A�����q0,�j����76%�$n�\--�y[)��T{�E��N����������V����{��{{Z��N�g%�M�N����-F*�V�p�E���6kj*�\9FW�k��gU�,*�u�,������;���3u�%������N�.�I����eB��	���&~���h���\��M$����EK�ivt��_nM��<��;�NKf�wkE}�Q_pp��`8u��*�{�u�~'5���������~gO];w����!�Z�v����8B��rA�A!m��He��V�����.�{}��~m����W����V�����fn:�x�cO<��������!�m�8B�*M!�U��mX����UbAV�8ARr|I�����i#8?
������l�:J�cgom���Y���J�Q��s�C^2���'���>�&�����Ho-R���I�
�����3�P�6���3��u�������]9�&<|^��M*(*s�Oo����v*�M���q�m�����u�������P���d*�$6�I��RB�T�
�r�i�}��M����E�P[�	�������Tu�Fd>���h�c;5�qU���|G�TT)r��
Z��mP�*���'g*�d6��qXi�_���+�^��?o�%�NgW#b���R�	��V��i^n�B�J�v��n���bB��p���4���C-��]Xa
�g]�&z>1=�ko'����tq��n��sq'������g�$�Jy��{&���}���}�%!��aH^j��-���uf��a�*��e��qSHo6�)�������y��k���w��W��������A���EU�p���Tx�\����p}N/N~t+��N��}N�P�UH-jN�Z�H]�$�xB�jN����=��y����:��:g_*�a�����>|�P��W)n�Z.�gA����~�$��� �U
!U�p���4��VC<���THf������;����������r7;��4����\��|i��eq�t,�}�2�	��	s����CmR$Z������U&�Z�$7qRiZ��7r��r�Q$�������{yE��)H��q���H�H	`��#M����:���!�@f��sb#���
�fN\��<yl�fQ���"b�����|�����L��E�s��$R��F�,��H����L^�@_g*b":��A��b	IQ�.����[�TAo�7$"1� ����!,���bH�1�Zs!B����d��x���Dq�:��^�u�;55{��+}U� B�W�"�B?0y�}W����O����
��w�_��l�A�F>E	z|����$o9A g$����'f��#���y��������Dck ��"
@���jH�L�t� �b��DW��$D��Lk\���\���S)=�����C��8��DG������u�%1� bD|�SA�1�LF�=o�P�Sr�������"��d""!Jq���y�}[GX���DMIw�|��h�PNMeT<`���D|��`�$�2���/QO����b#�������b��N0|�������")8�D�s�����X�)�@u"��_�t��{1�:����rg�Wf<�6H���|TD���l��B~�`J\���z�U���^<L��w��������5���:����Z�B�������������;R�EY��S5�������.��2;���;����r�!�tV>���mc�7%<�S�7�7B(���S"F\5��_yu��v�{2
�[�Qr���[�tR�B
�;K��z��U;��A��m��6������|��V[�)���������������A_e+0r������W������}��Ks{>�h)?��=u�P��)kP�r�4���i����i
��IHf�&"*=��q��o~~U��;�5�n33�K�Y���|���������#�-=���|����?�Hf���n���CHUj!�� �+6B���M���	�&.=�=�{�����������X@^�O�7
�.0u�"��]��x�_+�-Z�Hf�*���&��]B��Cg6�)���
��Hk��C}O;���u�|�������O��*{�7Ih�/�v�'�?l����M_r[�f�9�~�|G������L!k�C�L ��0�WT����o5P�7�T)��\��D����8����u�t��P��n����N���K�[��hu��	����|
5CH^j�d7U� �`��	���B��i�VC+��E�����~}k�&'����wZ(oQ3
��z�i~]B��VB����{�������B�VR���6����Y)
���*��������"���$�����$���7�\���\|
��,\���N
O=.�mm
�%{/��~<�����IHg*����!���d�U���Cm��A��,��`�[TH_}����������s���/���}�]{�6���~Y������r�{eyz7��N��}�w���d*�*�Hf�P��P��Y�j���ZC2�I�+�~���w�<���N����{'��z���n�G�W*|��'e����
m{o��~��#&�/H*��9!����)��Hm�M �T!�jrC-Q!��>���=�?g����St;]���:����������;Bg��os
�������MI�r��kV��`�j����C�&��5IH]��0�3���L}��s����1\��i�jD`5�w�� f���z��!B;�m�Dw*�:�A�����;��z����xz~��G��1��X"�0D��H�`�~�������{P�b"X��L�J@b��/;�#�F&���|}tkD��A��0OY����1i� �R�Ai�?$���q�~'^\�?� ��0y�:�*��e-M����O_�=��vf ���A�����)�=����`�����J�����X��"+	��j@u���3�z��%��[���8��r`�=���2�}��8���"8�B>`3������-�+������cy������'���}w9����l5]��`>�%��	�������6<`�bT�o$�+��1�"7�cYC�R�KB	z�Dq 
�����z���>��i3A_v�	b���b �@r��*��3��,C��1`�G�'*����{�mJ00A����1�G�D�g��)Im�OvI��$G}0����JZd�u�X�>z�B�R@J@Zy�@ZD|��8�8�:���O�� ��
`����"2W�Q�����*�`��[��:���(��-*���Qgl3��%�dL�]�^��P��4�+^;xo�k(T���hfj'�f��'�����������^��-�
��]���L�zt!��4n�7g}��i��C+�-)))o�������qu�G �Fns/�����|�)Vb�����f����*��\�yP�w����5�xp��ef[�kyx����&�pwgN�?=D�����p�.��j��j� �V��YH:�y�IH:��������f�~��Qk�Ok*R+G�[�k�;6����NV�v:�K_
������w�
������H[���w�Cy����Z���Y�/*�����>��E'���Y��5?U�p��/������~�'h�l�o�/�C�j6����u�{�|��i��8A�Xp��Yd1�	�gmZ��m�&TRG�Tr��onj��P�������v�}�k3�R�</���&������>���(�9�|�B��d�!y�R��5aH^UIHe���
�A!v�'c��!u�g�����;�fyQ�i���=���A����)���f�{o^�yvk�d��+��c���!w&�;VD�7T0��VR��n��!�jNyj�����wvy��n�X�;Q�@�.!����)��[�f�N�����0_s���4��T4�V�;�d�j� ��8C� ��y����5��>��5������GWc=��Q|�!j����y���Mm��bu;X]���u�"9���;�~���6��d3U��T��dH]Z��2�d���,�UD��U%!�~�����~OU������Z��)���h��6�G�����{h����s���$s��D�g<�B��j� �T�ud�V���f��Z��=y}��*�GQ��?{����/h��<�h���YG�w�/f��K�{��#���}��W<������T���!��JB�U���gy����v����}�-9��K+����||��e���A-�����tu�\��G{������j� �+
!m��Cu�&�k�6�Xp�r����a��y�Q����r�����d������Dq�@-",x�fLA�z�c���2y��q�"1 >��1�Dw�#�\���?U|�u$G��A�"�B\j��L��/��#�#�(G{!��~����ZOz`��y��{���D`!`5��"+���v���f$��1���bJ`�}!KS�G^�s!��C��5��������u�J��E�H/{Y����Q�SL_HDyvb#�D��#��7��2��g(�b �92AS�S1�"����
�u>O���_��|�1�0X��\v�<0o&<��K�\T"�0,P�|��jy��.�����c{�Q�P�`$���)5>k���vI2LB�u";�8���3F��H"�u=^���be�c���	H�<��H �U���>��i3�?��H>T#�F���$G�H�P"����������>O?���1�,A� :�FN>����[�`#�1� �"�"� �LRu�H{��.|��"��%���{�0A������L��gS�%�B �- �0��5� 
ab3��8��X�
m�DD]9;�[:*�Z�2���%*�w��s�L�*��+
&�D�-d��z���^��g����(%�y�+�L�|=4�U�~>�V��aX��������t{ x��!M��)��8�b�cH�\���iM�����I���@��r����t��__oS};��u�P�-�
���\Q67b9j8��8�T��������f4id9>{��Z8+��s�5�'O���dw�������*���S������'�~�kT�
qP�V�p�\T�C.U�H<��Ar���XR/��}o�M�/E�$W>W/D����V�?���)��8[��-�~s�+��d�l��R�����g$X$5ua�7r�iuCe�CHu�&~�����z�Y�":���5'�Ws5�n�HI���{��������u�<_^%�����
����Z��3n�a��4�U��
�Y�]XaV�=M���Z����]��,���6�+;��X��X��[��L�.o�������������}=�w��!j��w*��1��H]�d��VR�I������^Z�!�^��������I��z���O���1]�������!�3�y��7��N��>�'$�O�H�A]Y�����WHc�d�����	
��a��z}�����r�l_Y�K�<x���|��P�Vb�]	!n
������d��i��Q��������Z� �T6B�A!�*�H[�a��2�<c��g��:9��B���x2�&�6�h�`���L�G[C=Wk��=2����
Z�7*�����u�&x��!�k��B�nU��������;rK�/~��M>��v��w��v���wC?WG��v�>����^o�<��{���g��U"C5���[!������d7U"C[�!��L!������4���������d�5�@�)�)�+�W�"g/7���j��g]g���4�\VM!�u`a��8Cu�2Cj�Hn�P��H^5B�x�
C�;�:�u���}��A����C�/e��L��������f���{�=%!�u�[�NI�Z�f�C�-�I�3]Xa
����n��!��Hb�������4���$�s�k�*�J���isf �E��}2K=y���.Y��#�Y$@SD�Dy�����K�`��D)u�����=�
`���S����'�<rB%� �L���ICv`�
B1>z�0|�$G$�~�7��R}=�W*x�$��A�1�D{�� �8����w��E�{�����Ik,DJDy�� �]i���C�Zg���"
������#�1L?O���'Nv��Q~�"%�"P���!�q�y?}yS�3��������B���jA�X�":��1��g2I��1��v�+f X�1�E!�A)�T��\���������.d�����X#��d�S}����c�$A(�#�0$��BoZ	J�����z��-2��j`�R�&�$A[o��_d9�����9RDML@S�	@)���q�����W?\�H>`
H�B%��"��� ��?{>�8�_������Y��b5���i!�1K�#�}��]��5 �>b#!��D��������Y�F}��Iq�:�"8�����%N}u����rE�1�"1y|�P
N$T�w&�8���u�1�����`��"�2
�����%-D�D���#�{���Y��g���f���
��]��0�bm,� ����tN+����X
���P�����]�]�]���`C���������0)yY*me�I�i�4*�]R�!f��y��4�({��I�G��&q�9U��Rd ��xoo���f"��,�J�~��;}���W�A'��y,.V�qn����[��If
��z�p���x���WJ�f����p�U?C�=�,�
��#����`W������!y���Z�9!��	��8A�VC8����ua�c�
�c=?2����s��C?e�Z�]�"7�������N�n��eH���������ddV$+Z�����H^5IH+�L �����O�k�����=_�m~�\\�D5�y?id)�v(�wB}���Y���n����^�����^mP��C-k8A�VR�Y6AU"Cy���2� |�B>o�����IW������~Yv��)=�����xj����
�+z�|q�����s����;dd6����0��T�Z�Hf�]!���Cx�% ��&w�C�;��y��{�#>����QY��������)mg���������8P��X���@��P��Cyj�A��%!���!j��!y�JA���3V�8B��)���r�T\�z�7s�b��������X$o~Q0�����U{f\6���~��_��p���!��0����j�!��JA��%!y���3�Lz�����"���t}*5�#[����n�j�TCK��x<���+y�>���v�����I�3n�a
����Y
ua��C�*�A����MH#��h��(���5���s�ck}�J�[9l��*T�Bh�dK�����w��z�~��
��p�U���!���!��XB�T4��V�kS�?~>�
��}��:����L�������{�<n����������Q�c1���������;�uU}W�H�����T2C6�	���j�An��]Sf������;/{�
����.�{��=Q:oif�
�L�[�Kv����5�c�����$��Hf�Xd��R
��
�XrB���9T��T��k�0�>�}�����7s�~���"��� /$ X��*X��UMTK�{6 �u�5"����h@e�F�d�Db +��_0��R\�I��*w;A�2�Hk���"�LB�b�'�T��b~����`�O�;�����Ru�;s�"
@3eH�\�}s�1�����I�Mr}�E��}����{�DE$y")�1">��C�%�mf_�Tq"��GjH�$���������;UG���
�$'|����<�@��x��J�� ��vB2n�>`�?N'�N:�G���qR � :�I�b��=��'�]�TAHL�P�FL��1�H�)������9����9��A� �W;P$���:��=��1�|���e
B	b�D�TI_O������l@��Y1��DRDG�g�Y�|�;V�U1s�@�G�@DE'����j�9��5�A���I����DG������w���>���Ob�`��b jH�1n$�'��D[{�u2�G�S~��(`��%�Q.������ >H8�o��G )�MTN�����c���'�!\�C����!	N��������g�y�5ns���;3La���6������KW��9����@�E/�v�99�m$/��M���N��{�AR��8� �X#�19+�y�jr�Q�r���TDg���X�iF]�����<���C�su��3z��$l�I��{(;:+{9��w���I�vUe�jBe�����
�Z�j:j�wg����c���i��}�w})��k���1P`lS�)�y��.�%i���9��D������J�%8�����r��%=E ��
C\VM!�j�����;Z��7r�4���p�k�&�'�=�u��|nq�;����U�5����:��?oK�<]%el�����m��w�8��r���~�CT!��'$3j��X��VFCbB��L�U`����g�����������y���g�5���������'j��M��[�~c���Y��� ��p���rB�H����!��� �j����@�����m�����+v�����7��]�eN&Uv�qU}1���`����!n$	�����k[�1���>C�u�0�nU&���gcua�5���U�!��H.U
!�����{��g����$�K�]�������������A���m����RH> �d-�	
�S[��H<��R�Y2A�����I�y�{�z������%���8�y��ev�����Ab�����T�k�.����+����Y
��!�uCk��!��JB�+6C5Q!�U�H;Z�������-~_��vo�����gN(���UAV]���}�U����B�L��l1�i���T�r�4�r�%!��e���H5k';����uI�>�z5�U����
't��;�y��US��������3D���WOA�_�P�D��$T��T)�^He�"Ck�d�*�CWT�!Z�A��s��u������������*�"���O��
��9^|��b�f��{~��lk�n>�C�B�VCVM!�*��4��P������k�����}J~���g���?&`�Z���b��o��K���n����R4L���u���4��^�j�)V���P���$3U!mk�r��7���R������������j�����v�td7W�7���[��A���WOG���O+�u�cV��P]W�f��+�*�?Vt�����>un�P�D%7.�o�5����W���l��w:B�	�{2O�v�A���w,����*�
�(������UA`Da�of��0�0�*��;����O=���y4j��+
+�]Y�q�~��(�uDM�2��=��;�G������������x���4�0�T\>�M�K������g3�6�2�����~�����6�����u�^�W��^��������Te���v�$Y�`aQEXV=�����}���
����>�_�{\N��(��{��7���C!EX���&%Q!QV��M��g��;�,+
����]��y��AR!FXQa\�r�k��i�<��U=����rYV���B��E]�l_����|�\75��3O�^��yz�v!����z���Yy(M�Mc��x_/�jv���f��#L]��x�����=����_���DTAE/�g7����|}��0�����s�7>��w9
�("**����9G��o9M��2C��%f}8R�"��9�v���QEQWe�_{�pXXD�y��M��
*�d��oJ����*�%{+����*k�}�J:��}L%�Y��w3�
�X��f�v�>�#~��>,����m^z� �S	f������`���7��EL��s6��f�����A�XE�~�N�����`��Q�W���o���!a�D����RQ3�����AV1��v����q�AU���g��2�H��;�{�7�����(�7m��Mc�MB)b�a������x��Y�W���9U�fyDj�d����]����������yt���30�4mg�EM�^����&M���[�j���}x��qp{c9���yk�}�z��L0*�=���}>�����*��v�����ENw����e���EQ�Gnj�b�QVXU�����r0�
�*(�~�_�����HE�`]����I�O�r*aATb93��_�c����W��Ve�J��A�g<pn��+�������m�jO6�!�������i������=���J�v&����"���X�z���^m$j��kB(�>�|�v����Vg9��v�y^QQ��{�_z������0�{|�s&~&:0�
�*���~���DDXASg~�]��}�5�U�a�}���PTaDyY��"������BC��Ep�m%��'{rO38�I�~�����/7��^YT�j�r�����S��zE*�=p���`�]��\C�A?b�<���eI��g��<���^�Op�V�K����e�>����>�(� g6����������
.c�������}�{"5aQS������������3_T���2w��� �����o�{3j�����U:s��t�*�#�����m-?f�+
���'��N�m���P��1Gn�>�n���=d
��=���D���idF!������n�z����}]�Vh�9����t~�3��n�������E���-��<z���m��Fa����pTDDE�k.��]"��
��\����X;��{��O�cl0(�"'�=�f��*
0���U���:|�Q���u�yW;xsXU�X�~�.�=��WX�e�+����J�>�K���z���o�����q�)��i����:�����[��f���
��P�o�'m��zR��+&����O&��Z�������+�s��%�$(�r��N����T`�
�
*�]}�~���m��� ����I5��(����y�~��R��"���t���w�����*���APZ���fjuDUVF7�s���X��N^��FE>�r�I��Q]���c's���\����bkZ<�-���D����=:LT����O�D>�b��cWS��t����_Xb=��Gq�;��/�������TEV]�o��w����aUaaV~�w����c"�,}��;����*���1��-�{����0�&�����ox�n��(������M�N�*�#
�W?}��TDQQPS��i��h��c��u��S�t6`�9S�H��zm������o#���zz���WxI�D03>���K��<�s2���"�w_P������)��L�3R�������<(������R(���8d��(�(��8��0�
0���~��2�8�����u�����M�Lm�XHO�9���T�pUDaE�����3��� ������Y���0���*���������#C��;�(V�_������8��7��D.�s�"�h����c��H��ko�/�1�l�ld�`���n�f�z�S��M9�]�or�x ���=Wr����y���O%.�7]�QTDXVy�_���"���,��59(�0���mV��w��l0�Ks��r����Fa~�N3��J�
(��4^���U��aaX����a/����g�d�0��e�JL�"/�DP9��H���o�]��Zl[���n{G*VI�o��K��0P���{utlsN���:g�24*����j���z}����@�B�<����O>�����AX�����a�aDW}���~���
��V��5�j~LOs*UVQ����u�h�0�"�+��_��������[�:�0�Y~'���r�C�J�'��aV�T��h�oQ��FDQEU!W���u�����!�k��kV
t���s@!UE��F�j�=|���[��w1�K��A�T��;�y�*���t&M����R��l�A���baQD��q��+������w���;<�L���]����
����(}@@|8��m���s��o<6p�|��G7�������#E�abh|>���S+/:d3��3�htq�����Yy5j��
B��*�T
�U��7(��eh���4��E���������"����"=��w�����;�����j��A�(QV�!V
�6����o��}b%����
��u�X4]���^�}/��?s���Yk{��-���=�WZL��`/6n1�c�&H����mTn���wW�gX:�	q�n�����;���l��jt����3��_i"�)��-}>��>�#�����n��5QaUPO��s+������� �Wu�j'AU)�l��d�(��
�1��='�Vw����PA!�,��d��R���(�����+�=���2{UW��s�b�����M��AVXA!�t�m�m^�����+�f�6�u��M�\s��G�aETQ�5-�g�v)�C�I���G;l�n_rs�j��T�DE$�V7w:�R���cd�8b�.�y��:"�(0�(��������l���S ����v�V��;���Q�a�D���==�or�Z�ew2���i{�v�@�U�W�B�ef��]�j�B�+#�(����5DQE`\�z��s��o��6����d��<�(P��*���)u������R��&9�]L��o�[8������,0�,=/��z&����\G;T�"������J����;e�_^�c\^�R]+�'	P���T�[���{���^����#�$*�	��
��]�s��y(�^��3�������~��}��1E"���n_3�Z|���������j��(�***>�t����n��UXR��K���*���{b8����T>�R���E�{O9O$�FX92���2����*�"�*����6w
s�q[��Vd��jd�k3;so���,���HUa�TXA4���27k��'�~��n�Ge�T_���0(0(� ����#>�J�I}���ovk/l���9���jXUUQQQ�����y/if�y���:<��=t��H<�PEAF`DUQX��5[d����-�WnqF`���"��EV��}������&wi�;U���Oe�/������H(�� �""����;yS�d�v��+&������O/r��*
(��$ _-�6f+�0�J��-
�{�;;|��s%���(�
(� ��*��������������W)}���C*�
�� �Umq��������{�d7����S��	�wM�{l�KW�yI�����pTTe��y��o����e�A��W�S��Gw��&�&��{3�>y�9�E�Qg��CDXa]��s��~�;�t`QT�3��+�2HT}�}=6������#����S�p�(�����?z��l��(��15����&�5l,
-���.��FXD1��3mO�����w �QbQE�������{u�{\���������G�f��ETQXaE[�����"�dB�\��&m�L5_+
��
��oZ�w��s�����s�W��sW{[=���!U!�Z�@>\����p��K��
��{����M������Oaa�XT�oc�K���[�?��B��z�B����*#0��f���P��Zo�:��z"� �7]�O�(�(��*�(��������t��O�����g'y!EUT_���+t���jqK���)����Qb��**+*T�e���5-{
���V����v�����
�
(���Yr���={]/��}#�R�^�h�/�n
+y��l�i���hom�$%����������B�i��s��&��������Y'�^��n
V�A����Y�-�m^x�U�������d-DN�|�v�,QEZ��_v_�g�0�0,"�����dXQQQaxl���������������#���;E��V"��
�������qy�P����
��,;�����+�t��jjf�aXFaQ���;s=�w���g�[=���EQ^�����D�0,,
"�*
��+���o�4�n��!�9����E@���,(�������3��^���Np��l�X�T�aF(T����Fi���������|]��N��FX`XEX�����m�����q�����5&��
�(�@T����s����������n5��y�r2Q�VUQ�rs��v��<���9�����w�},QPE��EEL��'^k>�k����I�^�v��R+
(#��0�;�V�\�M�	����,����q�������@�`��uz'�6�\����������h;�y7
s�4�e}�3����8yp�8�;�#�a���7�E�����w�3<�Q<3�95h��*�����o'�����HXQ#w;�f����*��s����t�$TUF��k���nyN������,
���M.�;�����,����7<s��!�F�(>_��{J�E���!RY��c���s�g�AQaXgo.��\n�;�����&�9N3g��aRFU�a!w�O�������+=�Y��l�L�_{��T�>�@�i�7��@eI#|��X�OO=��
��[y�w�W=�����8<����8j��"�����G{=��s�����)W����y��q�s�����N(� ��P�*�U
#s����\���D�c�UEEF��N��v��Gb-N��v��z'3%J��(U��UHa-����e��E�XO+���z��<���@�
0(�I��f]����U���B�r="�Y\�B�q��8�������T0Y�3�(�������.�j
qI`���qsvu)���M��CHf^%��D@zb�j�I�����r��nn������@P���/w��QA�}��_���V�Qb������wu�E�Qa���s���������UTaF�g��y#�aaw�;U����"**����}[����Va��aE���Ta6g����8U6��b�@���kA��F��*�XaQ���v�6�3���=���oY��Aq��zn�S+6����QTXaQX�6�������9^�����<`�\=Og{�Nx�
P`QaQQQ�v�rg�wwk��/����J�y��_<����]q���TDTRj�|��6��#,�9q�K�E���==�DQTQQE�w
&�
�=�G���n2�onE�B�P�AU�``DI����'/��]���NM>w{m�|��*�"�*!�U�nd{4�RW'Ol��p_|�U
UQ����^�{��+9wQ:]�9�97��DU`XVaEc�Y(�8���:'{�y�.>�O[W��2{G�3����W8B`Y��m�j���e.��s���M:��hv����7�v0�;���|=�P�%�L������k�Q�Z����y<���0�"�)�������g9G���������\��t���E��3��+TTEa�,,,0��	���{�PH�("(��}�`�*��}~��]�"�"��n����	���c
"��"��0�����r����8m���V��39��GE�EEA0e>��>�x���K9niy����XaAEUD{W�n����n
u��+8=��
���':��� �� �1;�Q���^mdg_f^Wf����k���*�
�� �P�
�F.��4�p���#�r�{���W.�o���$,*
"0��:�c���gS��`���V�^��ns=-�����0� �����"���U�Q�k}���r��7y/�����������W�/,�m�����'M��c6��=�n�XV=Iu=yV��>�����[�Q,����"�*�"��"�f������/#A6J�s>U�)+���o���c=�ZU��%9��k.�2����'�S������W����Z�,l}��G���J��J�������Lb���:��&H�p{�^�93���^�,(���BB�����'����"��*|����TD�ViW����������0��M.��T�*����"���������I��*�*

(�~�����TDE��F[���z���*0�*
�(����������Q_%�}O���)�8����t�C���N����h�j�qVep���sfJ�\{��Y*){�;�\o�i����C
�ox(�w��Fd=�i�c���5���j';t�
J�w4U���'��3��/&����J�5���to(�_o$ ���e����o���r�Z����.�-6�KJI�o�vl��'�>Y�L�[����C)�VN3!x]�U%+u��C�(�8���*���&uj8�{�YlWA�^�N
�RR��
���W3F--�����sh1�)I�j����8���dj��$J�m.��A��.�������0W:HR����]b����M�h�C�M����6A���%������x��X�����N�0"��{B�lK8o��3��9W�P�e���� �#9y��>
#� ��V�I�r%�*����x/�kw+f>wx6,<�L����>�����a�Mi�o2nfJ�h����Y'������������#{��w��B��S�o��/E�����4��r*��m�B�q�&��p�������������.SW?��S�\ce�y�Wh$z����qw,����o���@��gaf�S������{��AJ�&sr����O:�Pk�!I[�<�����v~"���q�"/a��{�$���<Swo
:2��w���+�K��l�d��eZ2�CU���c�kE���.j��ap[�����.�Z7��N�#��	MH%,8�:���\�X��J(9R��D]�����xy9�9�<0���L��I�5����i-2�V���*WP�|�[$������T����<���}�C�vvVK�yer�@W;w����7\�!{�)7{��,
q��d5J1��q�U����+*�[���w6s2�i����"'K��@D�r����gR����tN�l�[.�gC�U�kH�Q��n����{�Nl��	�4���4�;���������������[(�����8,��t:�������4kb���.�PUuv;��;7���������Ps'U���;gj�a��72�|�WqhZ-��oN��wv��4�=�{5u��jk{)7y��B���jq�w6�vQW��5.�9�����I)�gqUn���h4����K�oFp��L�(����A��N��21%v��`s/(�tc�����<�4�F��d����h]�_Y������(����`������u�s������Bc���Q�I���x��B�(��Oz-��������4�z���q�,U98�[��tC��IW�r�n��I){{���N6���u���1�&K�S�<�U+����N��!5h�A�X�:C���a�U�Q���*Yz���i������R�to��v��������k�3&��u���)�)������8U�1�����>���m��w��v�soG���4���#�e��dN��(wJ�r����`,���Oem����9�-�r�a5���E.v%m���l���q�{X�p�cl��"`^��Q���%lQ�[*���]���V0�6<Z5*6����*sps'��M��-u��n�%&�����$���O����p���!q�i�� ��sM�w����yw&t�;f�M��l#	�o�Y�	���s�1h��s��+#����97���1��6S9����H�Y;������'r5�r%��>7zb�Jx\���=z[�����7l]��������+�z�T�Z��xR�b��sR�����4��-���������UU�Eu�1��r�r�~A����S����.����K��,��(��s6s�Z9����8��s/��V*��x	�������7m��T��;rr���e�:k#��R<�L`i�F����������`���z�,���{�;�6k���U��2�r�'�����������o*]�nY%�<�j��tWc;3%w&��$K:���	�;�<�����v��SKE�
D1��QK�`�3��B�A��]��[��|u37����~Y�\��q���X���8����w����A�w]llXN�a����Z+�s�h=��X�X�Q��.�B�E���Z��Wl��Pr�����U��[��g�.nm�����C�Ws�6Jg=�X�|Ff��p�;
�����c��n�-�Kb��]�q��p���7k9��"�v������>i��S1S�Lb@�c�uY��51�� �D�`��:�GS����d�]�K��]���$#_1����-$s]� R7���w����x�����DbAbG�~��[�DGr`��w��z�K5>�L�;s,A���#���u�C���7�n�8�J9!��0��H��z��� �G�+$�*���%4��@>�( ������ ?$y�8�S�����H��}�b#=��"������y<�)u�!�5QM�DJ�_d��#�I��A)�W�?}1i?D�n�-�)��VI���l����}_$As4��
DkL������fN�]���Db ���b�H�����Y����������b@Og�#���"���wK�1M�>KbB<������R�$u /��r���j}s���}PGXP#P`8����'�D@{<��(�O\����0z��1��">`�""���%�;�,��@���F0�  ���]c���L�f�E�H�H�b�"#��������g��$W$�P��H ��""X!`�A�1*��X�"� ���"��G
�%;�������zs-�����!��V����5M�t�L��Nm��C��0�*��+������OiLt���3������fk���rJ��Vs]Hv=���.��N�{�;��	Z�F��)}�u/U�q��@��F��.K���]�@J���R����v�]2��QycR;.�l�8��@��@��)�}Q���<W��$��D:�����IV��Dv������w���y����=�������U�#Ga�>_9|�����n�q���M��V$7]XCT
!\Vi
���U���*��!�+&�����}��[���H�_�=���Y�����ow��D`��`���f�1/�#jaUM_�|+�����uCn��d���!j�9 ��dX$7qSHg��5����~��k�&�-�6���d�����[���n�H��.��+R���p��B���-�d��I�V���5bC\VCx���������k�'H�H��!'��5gy,��@Nk/�|Td����Y7"�bDv"������Cw*�B��&��a�3qRi�Xa�Rp��d�
r��H�Lz���9����
��ng�!��"79�7|K��x��j�
?V+w����=r���>�o_���T�/6�)
��L!x�Cv�H7��*�d|����dsy�A����d��������Y���	b-^���o����j�����{�����{���l��������!��e ��9!�X����He���]Ra���{��y�~���}���n���S���o��W\ d���6������}���H[u@��H^5d�2�RiV�p���p���
��T�k�a�u�����������������f���noCP��ou���~y��y��rB��C7*�CU�p��T4��U�x�%!�*��x�!�PHu���~�}��������f��.d��=���PYxj��������d����y���g�|.A��T�C�H<��!�j�!��a�3�P���n��Hu���v71_f]nd��M���,b]���eG���~5>�K;�u�0�AF~��!�T���4��T,�*�A��AkRp�8��CVl����������������~,�'rF,~�R|�z�r�������u�����8��;�	��D����7ze���*7��6�P�`d��k��8cYH�1�[&�V�@�u�yg^�{����v�q������u����qu)�e����`q�w1���e�����'��cV�����W���
C/�U}�m�����^��@_Dz<�w����A���>=��H��T%�w����A�1�;��3�I�Q��9��-���M$/G��N���8*XZ^�ES���bsr�����V��=������vE��\�zS�)D���pLe����n��b}w��	��������$�CP)���=�K�6�����E��Q��F�����������.h5#k������N��&7e�{�S�)��X	��nS'R�Z���S��M�XU�u{����
0
]LL������X�E��:�N0���bL��n������8{p���;i���d��?���������B�����Z�W^=�w������0���$|�3c��Hn���5d�2����'f����Z�$=���{��}�3�0�2��3T��ld�F���p���F�����X�v�C����������Pd6��!m����'c���j��]\!yj�!��a����;f��M����3b>�(f����T@�q:���Qw�-l��S�&vp���b�#!Z�B�Vin��1���@�7��!WT��Y������_e9��~��X=������+��	��;�c��!
b���T�������q���'m�CH;ua�1��HmZ��6��!�������d�11�$Y1Q��wD����������%8�e:��������[ZyX�:A����������������H-jo6��3r�4�[���T���a��R���[���>~��z�������"B_�.h\���9�l�V+r��{�e��T��}�������|j������.�d�V��
kP�
��8A��L!�����{25�Y����V�}�vE�Gs����e�m
P��N�Yb�&=S�L�}�-��$5�go*�R��i�Vi�Xp���)
r�6C*�EVps��r/�5�~�[wOFL�]S;~��K��{��;�<q�q���&�����v�$Z��6�P��T���kHeV	���
Z���2)>|7}������E�//Mb.���+UP]C��L$qu�@U=�>�^!�f�<�=��!�m�8B��&���;Z�ujN�uI�2�XaMI�|
7'�|;��k6W�^�B����V�,.n9�_0�]3�t���c�R��T�=9U{��� ���� ��0,�j������j�!�uL ����T �D��u�|~��������/��-k0����#2Q-����+��L[ktw�Q���;�S��J5�	Db+^�d@V9���0e=��v��]~�y��f*^CV�A*������nS���u����T�q����g1��q�;�xV-/��{�M*���|W�R��9��
�0�8�z
�^d�mr�����Q�s�s>�3<�=��y��f�n��F��z=[���y����z���Za�v�=�+�[�$��{F��z=Q��vu_G�Y��\}��f���;�
�J�����+����Y�x��[���r��������^��,�5���}
�-[zY�r*�����qtij��%8����9X�f�'����3�w�r����Q�G���ilz��O�4b
����G���.4;��Ju+��VZ� �-��������'1��5g���������W�f������)x/1��nw�-Hf�,�Z8�7jr	|�g	���1��y����t4��R 	biF���/�H<��c�2��_��ujN�Z������Vi��R��i�T��Xae��C���T�<(��?�9,=���vh�9�����<�3u`eVh��+|��}�w�w��v�i�T2CuY��Hf�Y�.��nU��[TC6���V��~��=�?�k����8��p��c�^7�C���!z69�Z�.��nSB����e�~�$����m�B�����o-��3r�0���an�8C�<�{�{���u��^�c�{���[�_:_���c|���*\;)A�������QM��?/g����D��C�3qXi�a�Z��3�R��*�����;����OP�v��U����6�w��|v�z+�����p�\�]n-:	�t{~b��7�6C*�d+T]P�
UHn��eu`a
�T
���lr�0�~����u� �|��o�S��W'���i_��K��kv:XQ��k����i���
���r�4�nU&��U4�����V!v�� ���"�=�z>��9���<u������g�U�T� :�c:a�iQ��?�5)
]P���!�����V�]Xa���������^)5��C�C�K����oJ�1�-�V}m�������-������.!���c������{/|�tNHj�$*��n�Xa��p���R�Y2CZ�Hf�P��A!��������{������G�������w�;:�������gM����z~���Cv�HV�	�P�
���
Z�!v���3r���vAj������x�����5O�M�)����;=�cW�6�-\���n5��;�a�t�����Hn�P�UHk�d�
�����bC6���1��a
��)����y��^B���X�J�T����d3��Wj�������d�j��,���df1Q���j��	�����E�e�N;S1�T�BN�ta{z?}��CP�e��LN��v�i�(��7����5��0n]���E�3�#u�g���Ck{����nr0OD�P��}����
H<����=�����vfFtG���gqK��v������8�G��#��ND���;�a�"�>�3?�U���#�?{���Gj�*{���K��N��2�7�;�8mM�G�������8g��&����c�M^\TlV��������!��yn��R�l�r
�]41#l�{�Y�CS<�m���r�w:�$`|6xc50�K��{)��.]
���"����QMne,��3)�r�n	g��Q�0��x���=���}'�d�p��;�����r4����z�����v���/���w����U�t�VH^�2m�(���U���9�*qWR]6o1������znf~8����S?���Z���d��T�3��,�����j�����*���Vi �JE�������E�W��M^k��V�#8�+u������+�wu�n�
����!����Z�Ho6�)n�0���i
V�p��8(|
�5��
tE=����+��
�j�����~��w2���w�:l��ff�y��fL����R�a�]Y�7r�i
�Rar��n�"Cy���n+4�����x���u����4�������[K��c�9��Yws�����]�}��
��T�
���
qY�
�$��^*���Z��/�����r}��7��w�����v�oh7t�]I}LW\�T��+cbe��y�a�}���������sU�C5R2
�D�sU��D��T0�qT)]P0�n��C��}����_��s�������;�8��K�5W�fJ�	xX����9�!
n��S� ���QkP9!��kP�!y�B����kT�vC6�$/�^�G�bN�y�j���8'fY�-J��@����)m�5�_�����r|�XY��C9�aH:��$3j�![�B��R��ii�����������I����������07��;�������_l��������~���!���o*�Cv�$3qP��xA�VC5jN�uf��&"��s&~�v���y&>��p��-�[N���/B�+@������9L�����~���)V���N��f�k�a
�VA]RAn���@��������^�~��g�l	�{���I�p�?X��M�����h����<������{��Va
V���T4���4�9T�Cx�!Z�8AqXl���B���yO��7�
���>�P@w�������r����l��#*��
�	���b�f����I�o�c<!I��sN���d�N]�������q5��R]���5���M���_�$
����}5�s�����������L���9����W�}Y����4�j6��G���D���1�'������{���������1�y�����:���1����k�%�{����Rk�to$���}�Rz���%m9�}�^����k;�DG��NC<�Zp%B���eKZ�leT5@{�D3f���Z��g���X����>���/��#6�k{[�#}}u��
��e�RwYY���^�m��l��pf��,�LZ�m���F�e��[�>�
*��mj��(���+#+���{.������������8w(m(
,����V+/D���m
�e��F�b\rl������� �m�������^�����A���nP��3���v-�5�b��}�A���	���A:��QV�?������J��Hf�����C�.��aqY�5���
��kRp��Vz�O�*=br;'��[���M�}�>�uh�/�N.�O�]�!u�9!<�3�9���~�A�V�kY8B�k8C+�a
���!��e!y����
 �� ������=���y��O�2M��h%����K��a��z�������������a�RRJCv���e![�0����Aj������Z��<�����}����W��psL{����?��8�I�����728�s�#��R��[~�����HU��\V!j�����a�T)��)r�Cyj���Va���_�g�z����N�����H����Ep���J�
����5b�T����7n�!��H6������`i����'	����b]$o~���X��'���d��u��C_�S�7B��W���U�a���������R�XR�P����A�Vl�mX�kV	]Y�7�H>c�)I�v�YR�~����.�v_�;����N���� 4������u�W��u^�s��	��>����$��>	����Y�UIHkua�6��!��
ua�.�������|�Z�iO~C:������|�n[��~<��$/��7N����4��� ����W���d�]Y0���!v�'n�`p��T6A�FB�����<#�8�c+c��s"�����`�}:m������B
���.�j�0�JR���u��D�sU�!��� ��!y�d,���!�U�!��e!Z��O�D��1��?h�������3�y�UU�0��)���G��p']J����#������a�T0�sU�����n�Hg��2�C�3V��U�A�VM!~��{����js����w�������C�.���MFf�������K�lQ�N��#o���$4����#����A��9�H^���N��no��3U����}�hg���w���l.��z��F[�W�6? ��������;�"�;Z{�'v��u�u��j���|��#cu���.:������cmCp`��g�Fw����e������>y��}�te��8�������jF�a8��z�����o���>" ��s���J��m���3�'q��-Dzbt�����5��]�v,.#�*�$�Gj�fp{EJ{�BVN>��u>����]���X��y�
����	����0��f�p�?k��u�sps��H��1'^U�F�v6�����0M���g��Z2��3N�����:]����t����H����*k��z"�vR� e������U�'(��w���+p�<t�y��^�s!^�����2���a��C��t/�2�F\jQF����;{/4�����au��YR��_}���>� �"r�6CuX$UB�5PHcuCV��C*�!����������������W�8s������;�{��
�0r�9J�7����P�*�@��"�M!�j!�jp���0�5�p��jN\VM!v���������=�]������3e�Q�h$���P{�sc����"�	SH��������R�P)
�g]�g<U% �j�B�`���IdR$?~��|yb����Z4.���!��V������>@��yJq(~�7��{�������T0��WdVA]Y�Z����)kY���4�m�c�=K�O�����9��>�6g[���9��4�bNd��l#'�<�I��g�B�`�v�����2�� ��9 ��Ho-RR�T�B�S�\z>��R�5b����pf�{'�aNLQu���d&-��y7fa��+�B��=|��|Hb��cZ���C9����j�!��e���
�?]}Vu�~�k��g�)��;yD�1��NU�;��5�g�s�LR8OWG�|� ������j!��!�kH[V	�������vh���C[����y3��*�]�������3�~��7X�n��7�V��[b&}zI���������8AkP��Y�2������vA��s�C��u�(N���B�W��m�O[t��H�
r�O���?<��F������~�{�����#!UbCx���v���T����/5XR�] �S�3n���<���9�w�o�����n�P������
|����\�t����->y��!��H;V	�V�mT�
��Cmkujg5P�+Z�>���=~��~��s�����V.�Vz�,�G�6�������(��^T�#&�m=	�S�Z����k���p�1�D`1�Dq|m��Z�J<����v�O	�=��������y[&��#�F��l�'z*/�Ad	�����6���'n�5�nj�u�.=�luD���Q�zr���m�:������Ili��G�v^���j�����{2�	���u8��kKV�U>�3;N������
2�`r��8�G�(�cQ�����a;-]����e�����0�i�6����[X�a�^30<�w���w��.��5�5�,��M��/������j�_�%��=�J�*>���K���S�����Y��lV�#(*Q�!\���]�j`�1�wl{y��d���� ��@_s�����]��ic������>!�K������U�A��NftX�7��f��/�U���uJf�v�f�c��yR��Ww5��zVl�������u����p��L#]������Ok)5�x�~>�� �4U��;V$3�YH;uI�7V��
���/-P����mTH0L}�����T�U�J��Sx5�������g�U����0��M��6������j�3r��U�B�T��VA��)������5i"����/���rv����W5����������"��8U��r��w��|�
B�S�*�U�r�l���!���Cy��d.�FB���������g���R��^r�e |%�#���s�x�]���`]m|1�4:���C*�!����2�Y�-Z��nU�Hc�Cd�$-���s�C���p~�9g�K5���
\/�����D�O�v}W�e��{���\��{����������x�,�*� ��a
��rCbB�S��O�Q�fJi��G��>3�<�e�0�7���������3,p���W9��^���t���(��2���VR�V�5��mR���@���FA��C/������s�s/�����qf��j���[2�t~]6�{����/w����>��e!����UdHn�N�uf���![�+��3n��k^�~���=��N����;V�c������AO�*�T~�{�s=�y�u��������B����j�����g��j�YU�B���Z��:��;3o{������7���-op3gDjm
?�� Uf�`�0�{�� k;�ob��J�����>C�k��-���*����A��0�+Rp��*M �����w<���V~��
��7�A���s������������o����}��@�>��@�qY�.���Z��3�T)�VR5Y,���\T�CWT|�����X�S/`s���wW���T����~���N�]�!>�~BS�����l����/mu}X�(zYb�3;���Od,�3�iz�_gJcY�(S������u��n�w��������>K<8n<k }��������Z����b��`]c���e����
����r����OMC�*rV�Ep��&az=�S8##�P%�G�X����
�<_���s[Y���fUZ��G��k�,����G� ���]N2��{���y�nm]��%�~�Gc
����s�����9����n��0�c����R�w��(����#��kpJ�u%
{k��.������44����pws V;�2V����]7+%;����� ���l�$���U��Y��|d��
���r�N"2b�����q�n'{�v��Xi0���C[�������K�r���.�]Y���;��V�;0:b��1�� V�r�����w��Q[���t�4�Io6��9v$�C^�JZ������&���d�?=��|.O��$\d�r�6C�!�Z�$T�
�T0���i��&�|��n������o�i��������26�0�����GYI�w{x]�L���|�A��������j��k�&���H^mY,���!���!�����������B*>j�s�/uL!�<���
��l!V��"^<������H\�!���!�PqXi
]Ra
��8A��%!��CHeuI�:���@~��VA���N������.k����������6qA�:	3����_���7�XY�H]uI���g5Y)V��1ufHkV�U�A��v��������I��nvY��{���o&���*��������>G��
�#!uX$3�T����R�T2Cj�8r�Ri
�P�r��=>#�����>���������*c����Vl�����(�.�H�r�����$7U�!�����Hn�]!y�B����H<�K �D����=���m�f_oW|��&���J��f�W�T���'S����9��X���}�����C�j�!�
�j� �R�����4��������1��"�����|t��?I�r��w��q��_����A��.����v���{��}�4�����W5�!�Z�!�uI�U�A�Vi
��)
V���{����o���|���!�l�����x�2w�]
';��vz���=���P��V��T)n��j�![��3V�!�����!��Ht_<���~����{}���Pb����.	+���~�������4�u��nU���1���|U�!��� ��!mPH6��$6�D�����P�:��{�q��hG6�����c�{�'>fw����/s+�1���;�2�QJ�8�-XS���v�
/�F�qr�>����|9E2�
Y��z�;����A��O*�\E\.�`V�F\�r��q�/�s���������k�z6;��,=�y�j�%���l	��G�����
_��~����E<��,���O^��{�o�����������hk�Jw*pF���G�1��u{�f��A
���|<�!�#
��r�p�"=�d��9h�^�G�vmk,�������Jk.��2��z*�xeU�SX������V<�r��F�����-��#rb��y0����t)S����_Y�=x�}�1���j,��[��q��b��]�$�"����J��U���n��D���a��n��7����wwg���Z���{�,aM�R�.�$ ^Np�u�HLY�Z�H�W��Wx�N�/�M���0��u��y�I�����;L��0O�Gc~���3�<��>�ty9�X7bH��^	3�=�G�I���;VFCA ��CU�&�uL!v���R!���{��I�S�����X4��o]/�|�C�{�&M��|M��h��E{�IR$Hf���3�XR���7��RkP8Cw
!����=����}������y78zRX�-wR�U1|p���m����VgNE��������qT,��A!���U������)�T4�.�0�����o;��?/���vr���
�dP��D���������T3���v{���yk���$y��$�fE�y��Ho-RR5YH]�@��VM!���Cmk��������������M6���3�r�=AU���/
A��d�yLDm)3_~~��i� �D��$*��Xa
��!�uL!�+�7m��
�P�~�������*-����<tr��~��w66�+{�lP�����8���_[5�����_V�%
 ��
C.*M ����P�V��/�H.U
���$$���
�tN��R�������0c���w���g��h����-�}�f��o?o�{��Z������Z�!��aHoad�dT��W���]�'����J�u�}�*�<quNZeN�s}��}~��e���{��`�V���y���L!����
� �X$7�T)��������V����O�z0�b��}�b��}��h�e�t��������G�!��r�j�<�_��akP��UR
����U&��*�B�j�C*����!�����_�$w
����cs�^#{h�Sr���z���-,�S?���{N��{�8gN_����$7V�8B�T�A�T� �X$-�Bi�VM ��8Ar�4�n+4����g���~�y�Y
?.����q*���p������Z[S���iG�c��@�U����1]O^�3c��o(�}��J�E��c����[�9���������m���<U4�{*n��V���t������h�����}g��^��^�����Ogv�b�[��=q��)��^�P�"=}D<<��x�mq�Q�z�]���KLq�X���
�^1�{�LV�[3A34���G�*6s�t$�[�z=�F��t]�[!���"�,�r�y�������������~�s�77B��W�_S��kJg
&��x��uc��b�X����jH=G��'�u\���xn�y�48��p��6`5������nAwTZV<�M�&���*J�}Y�����Z���g�n�u�L����xt�Ro5�*][�]&:C�}�1��K�����������j�.���O�qF�*��\��GtY�;g����a���������:<o���Ko����n5����qc�a��M���m�w���
��	�G:��^5aHn�@�n�2CY
��!��JCn+�<�??����������f��0���^�03�Y��bP[[;F������Q�xB��[Z�e����T���a�
!�k�j��t��|������)?p����*{Y�~~z���d��g������7� ]j��Z� ���3kY��RB��8Cn*C[�C9������V���j������	{/�;�1�����P�n
��2����|�A]Xan�0�U����T�����j���RR�a��R��'�i����?B�Nw�7-y���{u�bf�|ct?-�$6Y�P��=y�����^g���}��JCy���n�C-X$7�R�[��-�gf�����a��0�������������>����:[[���7A�[�������s�R�]�����[�w�A�(�d5�
��d�	
]Y0��T�!\�M!���d1V$/�����S����v���ML���o/m���� ������u�f2�w����T0�]Ra�VC-PHm���-YHW
�j�!�j��;����������+��f���I���������`��v�%o,!�]
������2�S5�&Hf����7n��B��
B�� �Y �#�P�50Vw�����IW���\xT0�����7�?v�@��M�9�}�������B���n�0���Ck�A�T
!��!y��C5PHn�Y��������}������m><\:f����Y�;W?N7�<�KN��[�:~�rCj�2�H����<�e���CujN�������HeZ�!������=����BG�K�:S���
��/��t�;v��p��;���p�������R{�U��nM
m���-V������uN�������"��#���m
�"����7:Y�T��g���
�/'���>�}���DEEDQ��g����aQaXE�����X['�^�� �GY�UM�����a�E}�T6�}7��Q<�QU��f����9j9Y��)��0��:���s�}���K�^�]�h�����S�m�|K��9��C����ro�N�O~�f#��(�s���>�GA`��Qh�_*}�aXQUTTW��^}���"0��,#d�����DE�a>��O��vg�*0�*(������_�����VaT|_��sAaDDDQ���o�O��e^>hDURUQU['7�[Y��>+v������s�Y+Q�EVvB�7&��I~}�\�?���z*~#ve�lp�pgjn+pV�>g�8\��f��t-�8v�G���q�����N�A���%=�g*�wC����ov��w��**��{��s��`DQ��^�������0��	
��Y�����aUVg��}���s��`+,03]��gf�Ta�
����}������(����Q��}��EQA4��}>MC�+/`~���Y���n-�4o������N)���X�i~N���Sm�a�38��s��U�i�T���g���s��s(	�v����m����0�/�d�m���8�=sz:[��/���*������XA����ib�**�,(6��Y��!�#�^9�W�HDXV����{���UPA�}�eV���������*`���y��9�����O�r���o�:�����|t��7����)1���o��O!��$nI�1��VdQeNA�1=|&L#8�t�j����������z��
�Q]�:G�w-$��y�T��yG]��R�_��95�������WwF�S>'�-��>��Xa]o�'{��|tU�Ua�a��}�|{��DXXQA}3�����Ve
�"}����
(]���s���]�0�
������N�y���*�,0�aZ��)-����/�o���zu���sY����4�"�AIj�o�����uF{�MTA�w|'���ho>[��C��������j����E�YU��c�LG���oM��>YS�>1J}�}�4�'{�}�gf}_F
�����X*$(��	���)�F,"(������\���%EA�Uo�y���}�w��G��UUD����TaU�W&��36��
�*�yu����Q�9���9�P�`^ �x����A0�^Ssd�3FU�u��H;�����
��#3~���6�[��BgA2���+;����u7A�H��e�4s�u�9��M�����P�����S^w�l�.�j��
 �6}�����vq!�DXTF���qE��UJ6�9uS����*���~���~��
"������b����
$n�������������O{>q������b�(������r��^I�t3�n��|��)]�S��*��{)c#;K�������:�Yf���Y��AK0��	I�������n����x@��E8<��G~�3�(�����LuV-��3�vx���"�,"�y�'n���3�����������9R��"B��s�{�r~�B�(�}����^�0�"��7��maPEETXD�;�~��������!UXUX�v���em��8
��"}����M}�������������`�����ltM+
����8�C��1�<�K����_i��g�	^��w;�#����X<5oD��4S����p���g7����1���U�<*G?v�������.��aEo+����6W��B�����Vw��}�������2}���!A{x;��0Q%�����=�XED��>y���*�v�m9<�����UXUV����Y.��DIy����DKA������#z-��m�Y�J����MFK$���b��}��HY�E4�*U��N�)\u���'���q������
�$�}�wS<9��1�D�����FUDO;<�\�6��0�C���'zO.�������>��*��/}5���AA�����<����"��;�p�Q��a~�O[1�DR_zo�d��Q�%��|�w����.�����y��y��/����)��m�f.�3X6�p�;�dpWT��h��x���0H��=K@�,C��O06#9�D�LI���A��fbUp����o����U�Eb��S�J����s�+S����a����bzr�A=�&�����
**���W}���g���l ����ur�5��XT#

�����|v��V���b�s�fq���M�-���8�:�d�]��
�������V�>�/S:���$m���A���=CH�l[BEA��BX��@���vG^N���n�/��n�����u��~����������U��Q�F�~��V��Q;g8�V`���/F=������5��QV���\����B���C���p�DDa�Z�wv�?N�QUVaa]�:���v#��w�;��~�5!����w�v���q��/BXc��V���$;3x_CU���3����Y���*�~���>���w�VF�`�QPHS�V>����tY�FR��=�Ea�w]�APRV���m���UEa[�2���$Ta�z�Y^6%�DF�M��N�00�5'�1��g��s�1���=-w��p�x�TUM��u���bi�������k�=�t&�����
EDXDDE�/�wW�r;�s{����[��+�Qa�QQ�V�WN���9��{��=��J�=<�iU�:���(����R,����Vw8N�:�Z_1�Zx�HP@ATU��v��^�=��L�{}Y����sp�Q�XDU�HU�rw����do�s^���L����qDU DXXEQ�$�����VWjz/��,AF�c!������##Q�VQT����5���Ov��T����(:���;�%�J����!EFDUE�Q��r��hG�V]���7�%&�-b�vgM��(:�����B#�����{o�^���lqT�����r��������
������2��rj1��rF�c ��l�u�{�z�s��N��(���y����iUEUTVU}�w�7���/	qVDUC�����QEVTH�2����"������O���zI��XFXEN}\�q�TU_q��x�~����,#��s+�!.Q#
0�0��(�	�����~���.����${,�~�+z��;��XE`FaT����bs��,�	���g����e ��(�0���M�y�s�~��f��%�o���0�(��������Y��K]�v��g������_���z��*���+
"��g���g-�0h�T�=�������NR�EE��c�>��]f�9��}����SZ�y��z�MTTHF�E���p�	���m�7��O(��y�Z
����*mm,���{��Og/��_vv�����(������S�}=�'3��������i9��m/���aQQFa�G���H��������r��&�&r��T#�]��9���Y6�u^���r�mIG�{j�k�LV{��@��P��=��
��+��;��4^��
��x���������y?eY�$�
 "�|�����Wk���Da��w�+��zg���*��(��uj�����m]]��,XU`��}�;XE��"�"�/�^��0a�Q�z�?Oy����� ����ek_������pDQ�aPaX�u���|�Z�.�����p��*��"�+
�����{{g'����]����V��n���sk\DXT�Ta93�xVn���0���~�{f��\n��=���aU�DQ@f�1	b���ppt�Wh�Gu`8_	E�AHX@QQZy~�+��w8�1��^��������XQ�EUc�����\��<���:�����v�D��5P�(*#�
���Wy���6n\*�*�=G��q�k�;�l
��"(���
j\�e3Bpe�+�r
mW/���*��������N�<����g�6<M��I�{o��F~��
��*R8U���Xp�#�b�7������=:'��l����N���^����j�51K5�������F/g@t��0f�L��"y���$fm�{==���In��������v
!U|#	!w��}m���V�����=����B�1�����'U�U�Vfs�s��DXDEU3�v������a��]z�n��DaTQ�r~������*����5�s��>�v��T�TU��A���{�P�[��EV^��x�w7OA��������5XFRa�UU�b���j��h�s���RI;����w���UEaU}��p��|�-��i�fp���&����o�VUa�G�����o����{�����������XUT�!������m��Y����V;;9FlJ��
���(�uvS/A�o�Y�{I�g
w=3:\�pDEXPTQ�_����>�!�y�a����J|~�(
���
���I;��dJL�g��d������VAHR&����U��S��h������d�<vEP��@�+�������g�s���n���elmfgOK�V�Va���;U|����l+Y��������85��m����i��d��6�ur��h�|<����ey���og$}����I*���
�0�7�^�aQ�E�A��y���S��wk����� �+��-���������D�0�*��}W�_����z� ��*%��{����0�����7��/[����TaU�F:����O����%bb�EFX���+(p���_{�����vy�0���#*�(*,B�]��rjq{W�f=�wyX��j[�`�THEUW��"��$�M������n�z�����0�0�F{j��
�/�������H��0bZu���U_
PabUB�-�ry��6yY=��1��/,��0��
0�+�%��.yK@�����`��[[���:��������&�yy�_v3�7����{�v�y��IPV�@�<;c�b�Ch;CM�����a����
(�����
���w[|�#`��F�Dv�����FQ�h������x������a�����;T0��_u����U�r�n�=���RIk,��/�=��������:M_���`���n�XF���{G�7�d��y�{UrM����A!�~����!D��O2z����$�w�TZ�(���{Y�/_U������.�������U�.���h���<�B�'��_o��0�*�k-���R���T{$�DAXUU��]������o�W�*�n��M�&�z{�H�
"� �"��y�l�}\����V�^�<�`��</���(��,*�#
H�wMR<���:�w8\&�
����**��
*���%72�5���x�[M�l�+%R����"=�.*j�Un{�So��������F��EP^���.����	lrk��l���m���V_��DV�Xc/ko��m��s����/<)�u�0��������+(�0�:)�����y�������k����{������������,"��%�pr0�l�����H�6���������"�"�0"9�����f�t�;����ua��tr��'���*�����w�1G@l�G�"��#U��(��A�)U�0K��Y�E����as�>��B��6'%������n����~��>QEV��������s&�AW���|sr���w���0������wd�QQa}<�������EETUA�z�2QUQPDk�;X����t�`Ro������P(��B
���n��_���.�EVaDE`UTD^�G;�����9�;�nn�>�7����g�{p��*B�>V5F:�T0���f9�fWU��HV�EQA��)���U��u��Y��W���n���&�TXTF�E�o\<s�7<K�so��g���������`��{�9�0EUQF\���N�v�99�{7�XO*O^��i�,�TTHEaE��y�W=C�6k���'
�{[[��[�ef43�r`a��`TO�������#�h"������(4k�q�UFPUU����;��s<UxV!U�UTJ�����Xe���J���;��"�(���0�*0���Ng��,��������]�������������X�%1�C�y@��X�q�����x���8%�oww"��5{������W�\�����a���m�����5}�U�P�"B�/g�g=���Z�QP`TET���m��s�����m�E8"�0�*��������N�����#������W�\��XTR���'L�o�n� ���-;�eg'65U`Ab&f���Y�U�2�{'{2��*������|�=��w��z������u����T�TU
����,�����"�s1���<��{�u��.��=[���QXXQ����YUe"�e�^�@��i����dtaQTEE�UUn����`�Dm���]�H�5\����q|0��
����gt�q��v�������|��p���������B>�!�f<�����B�����5[@PUXTUQF�o7���w��}��M��J�x��N�2���VD��BEFaQ!Es}m���W=���;i�sc��9o^7�"��0��B*+�$��
�jt}[Z�w�]w}x�����+
�N�c]���������V����Cp����}j�7�[0�[5?�b��e�K����[)_\��6D�Bu�p�u��-�u���fA!W�6:p�UY`�h�4t����|#t
0�Ofo�l�%�[�����
"�"���,�nBX�AU*�9e�}�ij����,0���
#Sl����������
"#
�O��p������������-�Cw�����
0�����*~��a� ��#��/�����*�������>�2p���1#��l1����F#���LfV<P����tv�N�J��	C��l�]VL����l����aA����8v4���q�9�M��I���Z�����R�Ca�����[��U���4C7:D����BHY�h��!�75�{tH'5��{�yc�Qq�sk����Q��	�RJx�E���`}�P���5]�
;{*J��=Cou�[���Z����EVL��c�R����=�����zs��i�\�K��26���
] �}�
�w`�5���5|�u�F�w���k�Al5���<YE��[�	���0�Cd��%����%�U
�R���2�����c��N ����Bd��jF�����%�����,<�u]�h��v.�����r��b�t������v��a���5�G
�"�t�z+A5��<�4�3�.��A����G�wC�R��J2��*�b��y)�zz;�g�3��+*�/���gr��PGR���P�U�0���;41�Djwe�j��L)F�r��S�7be�j��@��bLN\^�6��z�^��U��W{�u���	��A��4b�#����{���X��9D���5�o%8r��{�oa�q`<V�V+T�����:�����:�S/�����]M� k1�
��c%�r���&6�=n���7{��v�)u���/8�c^c�8�u��V!�[�F��[a��38
+H��n�|���6��%��������j������iC�Qv��}�xR:�o�S7o����v�C���%]6s`-�y��]j`�k��B��Hk@52`Y��Mo���p)��R0V'�E
);�vpn���������3dQo+h����c7~��U`�d�l�yJ�����V���-����������Q��J��Z<���5�c�������	���(+Y\:�g*M�so������p�z�q�>z1z��SY���������Rei�������Mf� f8�L5�����:(�hK`�}������+%;��grr���k	��k���������X����]��hM��X�=��d�$kK@�a��'R���C� ����+]���t�0�w8�i�n;L$�����&y<}6:E]J���\��l�qj
��6��"!A��u;p4E9�'ff	�2�c�7���l�{�D������W]��w���lu�=9��j��gk9i�+��W�W������E���E����]���$���]S�<mD�fByM�2��V+��.�Eh�	n������,���:e�g&��K���m�t2��Wl����,]�2n\����� {�>�3��K�M���u`�����S�n�;���c�j���4]��y�t}��7��Ves�:#p	k7B���}�EJF��I����g9��J*��a7����waEz*R���w�������y���}"L�s����VW7����Y������z�sw�)d�gNeH;
��B��].),\8�\&�����2O�HU�s���C���Y�GmEv���7hgu��x.����;�}n\���o0U�}]�=��w��������6������nd�n���O���8�+}��WWL������e�q!Y�q,w���&#N�HP]���;��=y���-�PG(Q3����V2��.����c;N��u��-a�&�������U���[����[�e���E�<y��n��Im�(c�y.n�u5��x��Y�.lWm�����9�,��=e0���1�d��������Z��|�D���b���Yj�,�GB�:�R|�}������u��:A�7-��:�iN|�)�X�Lk������ft�e�(�'Omol���k��o\j��-,�3�r�������`���:�V��t�����{K9���7t%�*��Y�)p��4�y�>��f"�Lb���Xr�w���?����7T�#�I��r/v�c][����������	R�r8���r�GJ��NI�U�����e�������s2R�:��j"�������d���Sx�����Ij�mK@�����_@]k��c7h6��bK�w}j����6��eqT,��V`����9��r����3:��q�0-kw�.%!8e�]+_\;W0ce�U7(vv*��z�=��@���/q^q���}�7?�[pR�}8[����D���U�{`�*;��:���^��p�=�t(2w�$3<=��[����y5��*:������1��V�}h!����w�O+:�����P'60�u��N#MlL-��,�G������#�SI�&l��������C]�_\�\�i���#��u�ZW�7�
�y���.�!m����w����
�2�=�I�@K�gMk=�=�E�c�K�r��(�����9��y�q�Z���~q:��f/;n���P2��3)�c��*�!�9�����q���|�o��/2Yi��w������q
���)�ca��MF+��q�n�����1k���K�Gj����r��T6�
FM��H�����S��/JS_�#]P�Q��p]Q��8��;��Y��������f���#������f��n�=�<:��^����d���
����K����;���*wr�E��f_��9o����d\
jA��}��8���f��^}�zM!��8Cm��CnU�!�PHUj ��JA���|
''�|2A�w��7���^�n�����Q��k�b7�����/���O����]{w�st�x���X$3�Y)����nU��*���R�a��P��D�]/�Gw��]��������U)���uQm3�A��~����|sy�w����CHcua��D���H^Z�Hj�� ����T����B����{�����y���y���o'�Y�#B�����"����8X{t>[�.N�:�,�4�=�����)�� �+
!���kZ�Uk��$/*�RkY��}�|��5B�r��>
������p]��P*�?��o�S��y���/n������!�P�P�
�T4��T2AU��T)���5e!��s��O���k�T�v(�gZ�Lp^���;���~U�jR����m�K���b�&�[U!x��m��sU
Cm������+ud��C�6�sQN����-K}6wd/�*�^��V��89� ��������	��G���4��T0�nU�B�T6C*�d6�"A�T������S(�����Oug�Ra��������5z���!��K��Y���c;���M�s�e�;z�z�C9���Z��RiV��
��L!���AU# ����E�&<c��B{:��S��~Wv	��;��I��5�%�~C�d��N{�O��H*�����!���7kY�
�C�7����qY4�\T�����^��O}>����>x�<�q�~�F�<�?xb��7l�?3�g=v���^{�]�mP�1���6�����Hn����T�CWT2C6���uI�/��������:����%"9�i�>���Md�A�+1r��a�<B�Q��V8���zt p�Z����h�c$^���������R��7��t�.=u�/5>�Y�m�~��.��:Z#vyyl�k���c�)Q9�*�(�������*�q����3�D���rh�7�B(8���}_n�j�q=�>���M��n#�<�=��~�*6=���v�V�>-�����U��6������;��}_L�������������:�<�_�Oi��
W`�R8G�p���45Q���������W����&���e��	�p�D�|hF9J�[����0���)+�9WY2r���Y��\4������0��n�_M�p�>V�`)X�*�n^c}�U����<�I�<�����t�ix�9�h�=<]�j��/y�"��v����6o�8�$�+�����+�YfE^�"u�>[�K�h����
#yk�0�;��\k����.��[Od��vrgJ;���������NO����<U�!�U�CuQ!��CHm�f�n�Z�HS��s�{|����w��yd����=�c�c���2����Y�k��o�*]~�v��>8����r�]T�
��!���CWWn�SHoe!�kz������~9���`N�MM����-�rF��q=���7���J���Wwj��=4i���T*]P�
Z��]P�
��Hg6�JCW$PH^*�C��G�O��n��������V!]4�{B��C��cRu�O]hMb�@�n��A�7n��1���7n�A�VM!v��n�Xi
��p����z]��I��}���]Y}��`�U�Nv�����������]�t����r��{�]�g�_}��9!�X�m����!���Hf��CZ�rA�A!�*��7�X�z���=��5�y�|e�2�U����G[�Kf�nQ�
z����fa�u9�q���>E'��kTV	V��Z�!m�&Z�p��j �VC?{������G��N]���_\W(�K�M}���5�O������'(�L�k���P�#����X�Z��mk8A�T0���L!�Z��Z����aHy����y~������;�������=��B��D�5�O�M��,�����/'��f���3n�0���4��S�3��R�VM!�
 ��M �+�VW�}�S�����PI����g_,�����n���:/�;@*!#�����.�w�����H[T�n+6CUH���'<�JB�U
Cm�!�V>H������~Kg�y�.A����y�����������e0��gZ���tq��|�t9�����$1ua�.�Xi��d1THeZ�Ujp�r�)����`�^~�g������/�u�h��������!�(N'��I�����Tw�n��/.m4�j��|/V�U���r��/������qc���H��Q�=�s�Y�y�M�wh*��A��9�����N~\|������ib�y�Q�d�l�IQ� ��A�u�p�,U6� ��O�������3�������R��Y�n�>�;�����=�z-���woG�=�Y�%������{��7W�z+���z"���<�Cda���G�������}p9{���|G����!s��������
���e�k(�t�����)�^i(m��ef�z�q������^f�'4�<P�[� U�;.��H�kD?q���wn�K�������Xc�e�7]*4n�u!at��Du�x}����������u�����0a���4{qkU�3-���t�gZ�f�DvE�����m����W�����1� �
������}�}�	~�g�cP�8+�Qq����w?>����}u����Ho6�)r�4�U�f����VA�T0��Xp����3���>��~���s��������(w4�U�������k��p �����w�b�j�_9[o����������dH^*����9 ��!�Z��.���-���[T�;9�����eo�E[yx����F�NG��G����9�.G{�����]�|�z�!�U)�VL!��'g��8����aV����akS�.�{�����A#����g��Ge�	��nE�g�T9��M�C^f��E���=U��*��������H-k!��Xl���n����!��'����/UY�V�-	�����&�uR��'2_�:%��r�kn�p�3~P0���HV��!��M!U����p�k�Cv��!Z���NA���[Ga;�A���y2�w������"����v'��S�Q[���$���$����)r���VK!��U�Rl�*���b_e�1/~�1�z�%�Yv��7\3_�+�o
sI����^U����}}�}zY��!j�Hg6��7��H+�!�U�Hb��!��8>
5'����������~~>�����/%����Qu[�`�.x����1�^�����<��o����THm���uI�2�SHg-XR���
T�p
������7���s��}��m,�n�V��/?/������T������<�����0��Van�a
�Xi
��%!���H[�@�kRrAj�Cw*���t}��|��r��~��W.����{��
���MBk�N��kv��o�C��|%�`03t0_������Van�0�\T�!�X$.�Y�����"A]S<������9�v
�o� �O���)�(��e��+�w���2�2G���������wi��W�W%��+��c~7S�J��?����U���b������/�5G���D�jX������o����i���O%V ���S����8���KN���a�yx��e*3]��-���3/9�u9�N=�����������$���� �����G��Lr�8m#�r�W}G�[u�L�Q������2UD��}q�]�DG��������~�G�,�p+|i��|��B�mw��^G�g���d�����;�����{z��m! =�Y�0�KtF�-`�Ly���M��^.c"�����fX�.��tV��%��a�VU^'��������1j����
z�m����pP:���x������{.����+Ju��$�8f�������Ww����1b�[Q���v���w��K�$����;�'��Lu)s�u��-$����"v%p��x����Q����k�T����TJ���&G���'�q;6��o����:[����D�k�L!�jC�-�N��g*�p�V��7V�p�r������}����pC��N{���<r�3^$��Vc��/����0�d��V�!��o~s_����XR�T
C.U&���I��H<U)
���VD���8C�|/��~<�������$Gi�����8�N��O�M��e�4���z�y��O�{���-�����!�������n�aV��
��M!���.+&��������^S��M)9��:��coT�����5Y%fc��\���w����>��M������P��m
!��vB�e!ym��/6����Ho-���:|����~�{����u^�r�������{�@5O�H�)��k����2�y�~���5���3kP��I��Ri�YHn�P��T
C7&�k�L!������������Bu���������=2.�.�V�P���L�a�5�{�������7����mj!���n��iqXl�*� �U)��!y��~���j{�277�Y��E#���h28����W�P`�������-T��U{�9�m���UB�WVB��NHV�Ud.�$+�I�-�a1��|>Y"����f�����|>�7��B�~n�6��� h�q*��|z�����!nU�C8�R��$5VD�r�4�qP���<��!�b*=�+�<g����C�sa39=���*��&x��;�^)�-]s��o���]vYj�!��@�u@��Y�Z��-��l���Cy����R9��>�C�����~X����S��q�,>�������������j��P��b��wq���"���5f���^5e!yU
AqXC+�a��)�T�C^�:�>\�������h~���e�<\�������������n/�*�/��K����[���c����9��k�K^��{i�3	w�~��z��
����v�6y�����M;#o9z.��.�{����cY}��m���|����#��s;[U�k�Q=���G�^AQ�72(��5;o���\��zTKy[J���G���{b��ue���[O�qt�,�G��h
���z��t����k�6�Y��/G�|;����P�,wTUt�����]�61����{a��s+���
�~��Wh����mWb��}1����������{J:dzTw��������&�]@�����{cQ��[��$n9������f���Y��A��L�s�m���=w!��w;��u��C���w0�V���R���sZ�]����*�v�.�Zjd���[{�sF������MW���������[*����J���j,f��]��Z��-���$+��6	��X��b�����
��
q����7�����=���j��-��d+T�]Y�2����Z�H]�M a�}��p
��y~TmIl��{^#|���x6��WL���X��Z�v\�j]������}�0�����	o3�*��cT�bC9U�B��He�I��Y4����y�������~����A{��D�og7���7��8�t�wD���LY�����������]P�r�
���H[Z��ua�3n��uL �VC-�<#�.����r2�7�
4^pJ]SO���NO���<7��;���u85:��Sj��_u����������He�"AV�����V���RR
����N���~��tOq��p��(�o�����z~��,�6�	����%����7��d3j��Rp�qXl�j� ����T�bA��H%��,�K�~�~��<�o��w{�����uF�o��Cm���{*��l��_���!�jNHkT*�HoB��5B�����/��j�2vL�}����5?lW��"�JR�)������W��?2��h��!oN��w�4���0���
A��)��a�T6CUD��Y�qP�-'��w?}��8o����.'��g�<����~��P����_ ;U������{���!�jN�j��Z�rB�H��j��&��Z��7n����mI����6v_��v���~��(�+�M����S��V��BwK���tgg}�^�<�>��]����D��T0��VB��8C9��H-���mY)
���
qSH~��u�`����=��c.�Of�(�F�6����'�um���|���w^�:��s7��d2��AkS����Z��7V�7*��
��A��������{�����6�>j�D�.���
M����q~�
����Ug�t��Dd�\7���N��B�)@�����)eC�Q�Lg3��e���AG\=��N�*������e��s��_����ypfB�((&DZ��������U��[&.|�KOE�~��R$�~�z'6qr��G�#%{�q�W6mf\�hQ���������G�p-��N���{�u%Kt�*0�������	�y_^��X������U�*b�������x�.t�N0�8�|\z<XA^�rrG.i��?\;��%��5p���-�p[�#2�t�X�7����.����o�`�mL�v�g+���V��$������X����"��\�+A����p�Y��6��|�3j�$�D��1~JTX��W����h��Sz�~�l�P#ykZw��.�����v��������ygj�{��(5L6SV�!Y���g�;����f=%)[��{+�N�5��
����y����XDp��,\R�(����� �P���5UL�����Ek=���V��{���4��*��j�Y�H����n�Sc����j���	���:g����ic�����-�Pc`o�-�+TL)��]qF{���K~�I����)�R�n*C7*� ��)��l�U�B��p��Y.=�g
����;�|����|S��������s*�s����O��z�}y��:������y��A��8C5�����7mN�uCm����M������Hx���}�f�/�������9w���N��7�������r��D�}�R�k���9��`����H��5X$7�X�kY��VA���Z����7��H_���<����_�i����s�T����S��s�]�,Z�'��W��+\��f����a����Y��Ri
�Ra����YZ�P���%�R������-����y���~r����g��U`�T�XA��8���G�vN�+H[u@�Z�]����g<�e ��0�j�}C�TRG��^�wK���)ZY������[C�c�z�}�;b���n����+��fo�z*��������C�r>5$��VJA��;Z���C�2�P����r�i��?�}�L�X�1E�1!Qn�^�R"�|:s)�����}s���uO��yHf�$5e �T��x�R���yU%!w!���!�uL!�z�sg�������gZ]��&s9���I���V�
����7�_I���X|
�C���X���Cu�!��L��VD������7r�M!N�~���uz��f�GJ�#i,����y���w:����/[�\�{���2A�`���g^5JB�VR�Va�ZC6��!���Ho*�H_:�7��W>l���������9Lb���{k���z����{2����&��37Z�{�ie �
����|�N���x����/e�����|�M���:l��s
�K/�LO{��[e]&b_h�����C�\��2D=���%�<y���N-��3�i�'Y��M������������wS�=��������p���G��o8s7�����z���=1D����z=�.'C��<���K���������F����(�{��3v1�1}���,��em�K+��YY96��������[���\��J����I��Av���/dz�(����8�M�S��m����%������y�
e,uP����P�l��`Y�7�crgZ���{�c��q�x���r�KA�]��3.�we�b����G���3�Y����X�S�
��� �r/����.\���TP#������/5���I�-�b��Ky�/�����u�OWnPY�>D��kM-�����iX��p{���z���$3�P)�T0��T0���8Cyj�C6�����B�� �T
C^������~�w�o�����Qy�H�i������������:��\�����D�
�xB�U%!����Z�d3j�HUjp���L��X��*�u����k��z�|�#���D�v�{%���(�����[�{]Mv�KA�gX������>]Y0���i
���U"A�VL!Z�8C8��!����*��`��F�:��>������2���.^[�?v�s��w�������~�>��,��H�w*�H^Z���FC6�
j�!����C���������o���oc�;Y����8	S4�]�["��{.g~��}��^XrAj���@Hg*�R���kRp�V���Y)�S#��`!���L�h|vz/8u��������?f��6����Q�P}\�k����:}���RV��
��N�Z������Cd���C�7n��E�]�E���������]���5���������`J�	�.���aX�9��^�z�;����L����Z�$�!mP�VJC5�!�����'$�$=���y����x�����
�T^�x^d�X�������������������!�*B���
�a�3�XR�ZAV��qX��2�_|"r}�-��������A���&�}�O�����e�2���O��GG~>����M��:�AqY4�5�7��f�	����8B�T
|
.@�E#�>�>�V����K���g�%��]���)������F���7;0��������4���p���!w*�!v�������I��VH:�$7��R���zzo�|������r]��w���	�_���2�b�?]�����a�.�fz{�[��2dd�����jIbb�)7��BD=qQj�*q�0 �.w_��
�V��,����/.N.W�+!8�F-���^��p���j���+LG{���WZ�f(��7�/DlBj��������w��V;P@gjz�a��yU�����^���=�����c.�b�*�8��W%u������2���4%�^��vo������9�S�=��"=���!��^{v1����D�7��2����B5�E,�X���.�sNg7�v�����-^[2<�TBr�d����lg-V!���c'f��/�g������s�:����8jxR�����/%�v9aRw�A��vu��eU�J3q���h4�0�=�*�����`��b����U6��_a��B���������y�����(��>���z������wO��X�������V�rg>�vsv^s}��$wY�$��d�Cd��
�� ��0���Nu��r�4�^y�o}��~��w��M�]S�
>��ly�iTkN�}��=:��|`I�1������/I�����&D����v�H+�2A�`��uC$������]^g{��u�����`��J9�\�7��3�8X4ly��y�O��o:�e���a�6�Y�5B������CH;Z�!���b���7���7�T)��s�9��w<��U��������Z)d@����Ao�EV[�J�=7��!�����>
5$2C5T��j �k8CnU�Hg-P�.��eud��������������"v�=�������r�������`>����q�K+��=���_V&��mK!�T��)V�$-V	���1Z�!�U�:���g���Y����x�����B������
����89s���/���
n���T6Cj��eTkX ��A�VK!UH�f�!���?uk�72�����g��(zT�?&pO���^���r
n)�9g�P�:�O������
��
Cyj���YTn�0��T�B���/�&������1�^���F������~	��^�dT���I�k�6<�C�:�C���!�<�d�6�Xd���!��a�U���
�H�����&�/�]�'��*�����]�P���P6]�a��J�a7{76�C���)��mP)��4���B��Hg6�)�P�
���1Z��"���}�:�L���{�5'	u�������?�u2�PS"�����:�����^�!�k8C\�
���A��!v���4�]Xar��8\�!��m��;d/�xL
W��(��5�'��+�k�[��s:��\�8�\���f�����8���Aw^T��x��G�V��*~��b�{�f]�u�����v�!��d�j���<�z�xv�g}]�FS��8���x��K��+��	0\�<��c5&��M�b���U}^�GoF��++^�?{�XT]����j���D�'�e��|����v�����r~�X�����8�{���Z���q��GV�m���2{��C�>���Nlz=�K�R�����-�����er�~tY��1����W�nq�Es��`����=������UsK�wh��V��
@w�2XX�V��8�]��M�
�lr���!��kd�~�+#;��%���[�T��g��|�����h�/
�C���p������p�tZ�s�1�1���gV�1:�P��@���\�zR���<)US����kbT���5���J��B��j���p�����urk7j�K���#j�VC������u��VO����������OV���F�
������jZ��6��oaH-���T)�O���}���o~bu��hs��]�:���aI����.>c�u��^�6���j��C�.�P0�9T6C*�$3]P��\!�����# �dd��& ����]g��w+�N��8k-�n���;�����;�}mk��'U�9������w��,��A!n+
!j����![�a��)
��akXp�5�8C���{������w��Bru������<1:/~�|��J�7���M�oAY2^mR�������+�Cd5T��an�2CuX$7r�4�V�����g|��;#��[E����W:����q��P=8���)���(�%���P�*NA�!�*�����$2�	
�VB�jo-P�+��H[uC_q�'���Ws}�vN���%o76��pw�?#v,�R�l�[`���-�����\�uH]�!��S$3Uj�A�`�m�����-P����(������U���l6�_a���j�� �r!_�G/�oeQ��}N���:������YH.U�B��0�5�p�V���WV��$X$-��������������7�U�
�=[jGq���������@����k�w�|�uX$7qP���^*�����e�gUj^mY)
��!��c��?���������3��.����1�}v�g������y���`g�z���JCyj���!x�%!��i
���uL ��
Cm�&��N��y���>����:f�0�8����7������B���n�EVv%&a���w�|
G �����N�k8B��!U�7�jA�A!���C6��w�����~R�������+��b��_f�?_���
8y�-pjK�/9�~��9�������6u�����5*��g�����+�`SV�Kq������l�e/�fB�f���kK3�����v�b�aDh�Q7�E����K.�q�E����cL��7if[�����q��i{K����m����#����	�`z=�5�lj{8��7��G%�ST��A	�95����W��DA�R���=��PpZ���}���^y��6�z=WZ�	c I�����
���#�+��i-��/�������yu�D��w���y*%����ZF��.��v���!Z���=:^��o���W�S�80
��.��I�����N���z0^��-��u����A���
$(v����:�n\2�PN��&j}��S�����2V3}r�J7V3�����v.��p����!
�]*��/ock�@Y����[������^��5��.
������F��[0k����w��Cv���3�V��Va�T�A��$2�P�u@��R&=n��g����3'��z^�Ka����}������M���T�U��&^f�z��s���A�VL ��e��0��YH.U�A�VR�P�	�"RH���k��~�����%��B2��#�/wn~�_/>�U�Y�o���c��w�����9��02Cj�He�C��Aj�
�P��mQ!U� ��!�V$?k�o�����+�o��R��������1�<�B��T��m9���$v�L��t��>
���+Z���Hk�CH[��H<��!����-��������/�?i���>���:��L|���n��������L7�����(em}G]Ra��2C*�d.���mN�Z����8B�T)
�P��N~�S��K��������������?�7�;������c��mQ<v�2��|3��R�VB�+&����!nU&����Cm��yj���U�|5O_��F�WY���]�}I��c���[m�Z���G�4��7 ��o�����$�
�"C9U)���!U�p��T
!�jk���['��@$����A�&z__�C�AM����uP�.���K�|����������w�}������jN�jN���^j�Hj��A��
!mk8H3d��@�l��=s�qO�3���$�k(�!���������&����������u7O^s������y�e ��
!y�ad3j����p�k^w*���P�V��}����~��~���D���_C������<z������!����:s�v�#�n���C��}C�!���kHUX$.�P��Ri
��p���B�S�����w�c��W�����������d`����b9�;,f^qzgjsw ������3���p�9t������ ��7��o�{~������4|��
�N�������"��**e�}�n3�c���(��(��1��2nH�Ab�'n����������E�Q=~�;�]��"��
��0��w}���>��XAEb��Yft��UU��<���vejg���c��;����\�Uv��T@�	�}������.^6;$-��s:sG�M�KT��nV4�S��=�����%>2��y�G�����/�%�:�K����*�*��e�|�}sJ�,*0#������4��
�*������>>��4aV�S���z���TAXQ�����\��@��#���7�y���`uAF!�Wj�}������*����w�[s�FTTQb�����R���g���1�&���k�&I8s%j��du��=O����5�#B�N�da��!�����s�zQ�}��P�^/�i��1�����T�������,V_�N���y���{I��^md�}�M�aUUU���%���.�XE�u�5��=Y�aX�UC1������{�H�*�,*!���s����W�,*,(����d���QXT�w+�JAXPTn���$���cg���h�}� f������{l*�����:XA��#;���/���"��7������_S�s��zp�n��(��:p��v,;��3�*-�.�4�:�x#cDdn][��w��{����W�Qa@S���+&����U�s�����_	DaDLg��������wTXUFR���/TDXF.��������Vy�l�Erjyw����Q@Uam�o�����fO�>�,F���d��Q����IzM�wX�.u�O�`�y��4�/����s$������=�g�Q6'� m��1�5�z��7�u�� ��#y��^�e�'�}����u�*0#�/����DDaFS�����s���V�}]���Ub}���'�W#����������R�+��������&#��*�����o-���}��FHaUc�g/����o�qN9�������)�.���
�FO ������Nmxe8:��N5�����`�>��Oumh����>R{���?~W�~|���q�x7}7����s���h�^��)�s���3�Xa�T��c�/����������UV��y���F�*�(��/��w���aD"�*��~��)aP��'y��QE>�U���+nUF�t���T\Q�8K^k!��u�uQb{���HW����\�b���`�s�q��*����VVEoKxn02tn<���k����>e]s�p@���<o<��t4N���EUS���gMXPUOL���8�*����������aUE!i�e]f��o����U��v��/�}��,
��0��</�6��E�ED��v������������.��2}l�C�d������?{"�B=��L����$�<�]������;�P��YR�t�����r��fl=:�@���nZ��$Sr3�-O+�v3�������rd��5��#������QVaFFV��[s�w�"�,*������*F�PE^e%k��Fk{^l��h��*����M�+�j��������S����:�0����T������������
(O��Y������?']�����[���Z0j=aG�x�*�?{sh�MS�F��j���Z������R��;{}XW���
'�������-������g��Vg7����������J��(��SzL��d��,}�������0��, ��[^�����$*"����7�;?}���EA�R�o�I.��C�f��%9F
,"���}��������,#_O��md/Y��WD���}�W"��|w���*����
^LR'{]6��m�l<<����K��*���+�S���Y�t�����'wuX�.��������f�o�Y��Vu9�;���RTW��|���mmv�F���������`*���"�c�n����MTEQTV��9S��q��(�UA�������)D`XULm��]�������y��(
�� �����P���m4�B�Mw�r2�^2��s��
��b�2ly��}=����#����=���������T��3�Tv�J2�����&��G�5��c�{e�U.l:��mw9��
�k�;�>���-�{U������"'�s�f��
"�+;�j���
�Wg�oSr�T��L�\�;{V���(#�/���{��v�UAETR*{������y�)"����l���
���D
�~���AWM,�TB+��[b[��n=�^�u���;OG�i��,,,�~	R������uJ��]���E-�v(���5�f�s��k�����{_o�;��-����"������;����'�*",�_�������&zv�k?q�XE�D������w"�D�g�x��V�$*����n������FD}G*o�M��+.<v~�/gE����|��&
��y�4gj��{o�kgNra�Wv����P<ud>�������5=c�]��/N�+OP�v�u�jq���B������K��_(P�gf��Y����UaXy�Nt�VUh����m��aq��v�DDXEDaaT�Mf�3���� #nr}7�28��#������s�UK�(����1�'>���D\�]�s#�-��K��`�����o��UQEQEF<��O{����OW����v�����f��!RQFB��T�[q�5$�5��*��,@��S�^�EETDP1�B�t��7�
h)�Nu*�5�XPXDTD�mOg��z5���w���Y�-��N���
�+
B"+�S���j|z����=uu�mO+�� �0�(�Gy����|�����0���uI:q��haRXA�����}o����=�F���wy9\����*��0�G����4�Zu.�r��2�U�����F�VEUEQ=������&������+�
mvX:;k��n�|�A��z�)�'G�n�����;h���\�*��Wiw��Z�����C�j�ens�>`��t��{��8�#��\�/����%���+�	%� ���
�d�#�( ��H���i���ADTb�~�=�������sr�������Aa����>��JHRa
����7�e�1�`TDU�E�J/��wNB*����<m�^<��Yh�**������*��+��3ok���{��������6����$���*����z��y����;�2�[���SM�yS�X����0�0�R���'y^+�{�:��W<=�O/LAaXVE���'2���V���W9������%U�/{���7��00�(�
�,"��S�yzz�|o�\�u�]$���&o�}��*B0�"�~���v;lE,����.�ll��RX��������W�������wY����7�����t�\����(aF$����}���y�j�s^�b�r�������(����<�F���z�4X3����>�AQED��<����N��^L����8�K�wF��&?Eq��J%fvN��*@
��=H��;��I��1������jXd�D�9�+����U�$vgo�����N�_X�T~�9?}U������������~��8��"B�f�/���;2DaaDX5����5�����VQG��z�/�rDTQ@E�k8i{��0��)����DDaQ�(��Q���OL�, �*0���j���{�O��O����{����s��sQQF�j�
�6�`�l)�f-{b��0UUP�",1
��6�y���]3tL���2���v�;���+*�(P$���[aY�{�4`(c�q��b�Paa�DjY�r�������o
��V��M�"���(��'��y�s����^����W0V��2��=�V&%T�EX
�g9�}�3���+���92�2W�}"��
���"���������=��y5���e�3�w~7��U�EaAFQ����b���p��P���wP����0����{Y�qX�XXTa`}�������������b��c���v�[������do�b�mG2��F�C�\I�ZoOeOM��u���K� \�T����V8��7>�I[��l��*���3���57��a�S~�w��&�������*�wo1�BB"
�������\��"��6W��������#��(����o��9�e(+�����8-��
�
'D���6}�n�aQ�DDQaD$��
�~�9CR)��V��u��Ru������aQ�Q=�=�_e�Ofx{����������E�D}���j�	�9]����{���\��w�}���
�"���9������iu�����6.D��
>�(�
���
��gY�L�._��V����_r��\�����XQHQEaEa�����s��|�&o���s9z���w�%mn{��0������u��/]s,���);���whoN=�VQXDXaQa�l�=C�)�
�oJ����a)b��#�vcHAVDX���'&y�����|}��f��m�l�"�����*	8��g{��]�=��t��{x��q�9p�����
��'���������X9�pF)n�yud�Y����PXz�,�tG�am��X�r����^�0������������g�mf�g/���j��
�,B�����x�[
���||[���oX`Q��}���}%�V]�����W93�EE�TAU��~����oaEF������yS���~�EUPUE>�5+�'=F\j�5�?I��ndFaF95{;=s��[�v��Y������^���xfAADPQDUDR�6���vs��s�[������y�����Pa��>�h���M�]#|����dg^��O}[�s;XD�(���,"��{�6�����0�Z�4yf����������1s���������z%mf>s-v��L�@���B��J�,B�
(����3���};���]os���9�o�u��FH@T�HVg���E�*Edm����kh2r��0XXETEEEaG��K���M���%�^��n���H��t����"���������{p�v��N��X
���K�~�>a���V[�����������r��2UBxX�P�=���v�J��%�dS!����,��vnS�t��d��a���'�*���5�\.-�����lU�$�R�QK>�+o�B���\[�����[�/`Aj{��vr��c

��"r��6K�hqDETV+/�S)��"�.��s�7��B���
>;=�����TT2o����rpNG�	�aEa�UK����K�g.�>�3=����{�xQU�TU���y3������/;S<�vNU�����^o��:����b������������:���s���ez�w�{��j�����*B����P������0Nj�'�rs��@%�+�^����g������"�0���>���u��(t����|�������EEFUHAEbr�>�_P�wVF��|�8�p���!TQ�ENg^n�.n�d��d���S�W;"0��������(�n�����go_Wm[�q���}���w��>�u��*$C0��  g��m�jV�x/�pY���B(�,(��
c���L��{+��7��\f���F�ysk*N�]�M����rsr��H�s��M�T����9�svD���J��C�D��ry�}�������>~��AXEQ{��f�O��=��J����*��{<n�sEa!����k�V��(�1��}����p�`Q�EZ}����a!V���ez�(���"�zxwn���$XUaP�'{�����m���$"�("�7�����uuQ�e:q�:���yV�	B#0���*�.�����~k��v�}���}��S3x������*( ��}�����/��������oNOjk}��6h�w��tFAAD~P�b���j��O����p�yh�J","�+�s�w�3�i�I�;��^���>�������b,"��+�(�
f6>�	��r��l���g;�w9��U��QU�UPPy��%,���^���U���+'Vt�rY�����E!TTQDaQ�Wj��y{I���n�� ��\b���<���(�0���OL��7}|�y�������y���|�*��*�*����pE�������0�{��7�cucmG%

���$�e��Vo;#$x�6%��y�����r��w�%;�X+;�o��CQ4%���9���C�)�{�����"��)�8�>���U�EEv�\�w�_{��� aE8���O���XTUPJ��|���DaK�5���*
"����s�M��Q�S2��s��k7��*
���g�OmFd�""����
	Y�����9,������)���yTRA�Q��P�XN��]�Rc��(��������'�gETEAXQj�[K�@��s$un�4����&��VaEA�Xb�������o���r���S����=�QPE�TaE��W�g�o��y���M{���s~3�o2�5������"����s��i�U|V��xr�;1'�T,)�mg<�W;��m�����������������y��b������"�*������������������S�`PV��=|�wo��}���{3�7�x���������VUAE�AV��{��/p��v�ImPYoNr���s��u�������Uv���=�qn�7�Eg[5\�*�FO�r�o���D����h�r<���e�����-q��wr)-���~��W��>�����"UQFDaE���^5�g��aTaTTDT_�������UDVQEAX\���l�]��l �"��
C�w����UUDX9o�w5�TU�E�XQU��U���������E�T�.���s���uaUPDEUXE�Xz$����k**<r�+�^+����ee�N<r�-G�7�"�wsZ+���r[���s8f�.Ar9l��3{���+(�NG���w:7���s����9�I�+/B�u��8o0����*���:��=~���;*-�w�0AGu&��7>��u��%\�����&�T�Owm$��Q��IWO��/&%������'���qY
��k`���(l��J3;���R�1(5����kE��Z�+��R�$����\v	�����0���Ov���V��c����[�j�9�����zF��������A��,��{�����#����N��-� ;�%:}����L�#;����M���'+�2VX��
v�����j����8J�u� �b�`|q���s7��y����}�m�����Y��kz
�]�������� ]9��h�d�Cs��ye�����^��vS����i���[pGnE��g0eT��3���fp�u>����39���j��]�Pz��*l�`���a���L����S4�m��Z+��:�j�u+#��T��l��6��;4����V4��`�G�#Y�����A���j������X�8��pP�W@��Z6�a�{uq���|U���V�����P�+�e����vyN�����D/�������d��w�H��cf��oL����r����zjY|�TU�����}�+@#���
��.���D�Q=) ok����X[3f��J�E�NS��U&��dT�t�
iiP��8v�gz���Y`�.��������49Z�O����}P���	�mWU�88
h�R��.�-��hv���*_b��=��S�^)�p�X�v#��l����g�NM]����o�CkN((����\��J�#&�l��l��X����e_\�CNN�]`��R��Z�f�93Y��veD��4vWehkOgP�+��E�w7S�N�W�.�����x�P�p�d)T����l���Y�����le����������.C)��i>����Ydu�)��t�,�sZ��a��,]��b��g�`��\���<4�k��_Q;0�Qf�1Vw}��ug��K\�q�0�2�����#�[������0s�t�5���0���Y�)�_��yu�Mkr\S.5������D����s��J��W���7�O+��BzoQ-e��x�&������[����j�td�����r�AJ�hR
����8��	bG����We2$]��yl'��^��-��M��'��K����t�C
	+�v��
��{�=��Mb��U�h���\.�Ir�cu>j�<n���l���I��N7]eZ��@�\��.�����3�Pz���T��/�M[w�����{S��DZ|Q%��tU�.��n������6������"�[��J�b��<�T`$�q��C/�+�Nf��\'rn��������Bn��]�QW����;y��.\kE���L�����7x1����s@���'�]�{���]�O�����Y]j���
_\��e����\���8��bg���[���c��xNp��6Z�8�s5gt�TF�L]	������l�x����nu:l�[�g4 �;������M�.��|_-o;!��L����>�2�R��h���S�)��}���:N�X����S��J��r�����������L����Q�=�/U�Hh�{C��{��J��nAJ�Y�ig
���w�Y�
<D�xp{6��d��7�!�	�[���<�P��������%Z�uvk���wA�a���wJll�e�;���z��W���l�L�����O_b��b�R����B-v6.Az	#h�2����� �\�Q�/mr��Q�a���v��i����nd]��*����@����&��������F�|:�m�� ��Rf�;����U.�Y����Q���jG�L
>ss���4����1��ESut��|-��f���H�iv�T%�����������E����9���q�	�s��oM3V��H�%ps����.���/���� =�����U�T&��� ^"�{�}/�Q��2q�yE��R�;@+A=|�[����I:]�hp����L��T`n��p+Oe<�����Iv)Ct�*����Qxk�hJ�!�Dw^S��i�}P��=~��gU+�Sq���h^�w���g�]�J�]{%p�[���+&`D�����������8�4kf4j�?s��/�^�V���w����c�+}N��y��$���tV���h	��|��<\���i&������z�L�����y��"=���rQq;�z=���LjJ].����Qt{��U.#����o���g��G8�z����������6�=�I�<��=yi���}:IT_F�s�{�Uxv/z �2S&zw�����"��#�E����nKr ���O\�c=S!�}�k����cr�����u�UMr��#����U�:R�.�h��|f�I^_\�8�1���V�D���]�3���_-�����s^��%^��
I�+oB����R�E�v����J0�Q[!�zAO,�l_�_yN����w����X���'���G����z��"�a���|�Ij�u^�!N����f���h�	f�_�?z$j�RufHf�"B��
��TaH^Z�H5� M�>���-Z4/�uB����
V�{;��Z���g�{r��'��r��%���������8C�9!�R$7V��
�VY
�#!�V	
��>c�i9>c����s���,��8�~-���v��GxO(G,T'�@�#�|���<����^@�Z�!\Vi�VJA�VB�U�A���
��|jW�S������{���"W���m`"
G7��|���J>�|D��)�M�w�g�|������B���<�e!��Hf�H[�@��������
C�p���������6G�u�'6t�M��k��������'aqA �U�f�.���}�P,��`����j�Y�D�[�!yj�!�� ��*=�
��������33��m�
�22����k�V�O5_$�u}s�_����H�b�Hf����D��Y�
�$.�Xl�j������zs{����~���I��Zw�P����c`�T!�;s�=[�-]O�w�r��uCb��W*�HmuI�j��k�L!yU �A���|���ZE��kR���>�e�5����������0w�@dM9_�)��w��d2����AqP4�sj�Hj�'6�D��a�]Y���(|;G��\���{��u<js������p<t�e)F�P#a�}Xd�uo~�nw���k�i�go5RY��	V�����VD��*��+
��V	���y|��s/��0����
��c�i��P��7�fU��o���������f����AY���cJ�HoJC��j�!w4���������C��=;���^��IV�:��Y�����r�����Nkx��M4=�+�f��>f��[���n�L^h������{���`ER�j���������f�	Br�ZU W�y���������U5LZr��<�5sq>K�=��L���,`�����d������O��{V����
!r�D{��;���,��+~&gy�{� ��9�d���{j0/G���Hj�{�u��{�O�T�<�������9�{�OgE��;�����G�������R���a�h~�y���k�3�#B�I��jv�;7���@�hO*��z0��{�z
z����GWZ�$zQ�I�;�_R�t�9����aH����_��m�j{.��E��Y��XyWQz����.�s;�s 1[��k�L�>u��Z[����k���4�����\��K�xV�����U,]t(�*{�5���j���<�',�,m��q�N�h�H8�e���9��e��������<��M�'���2�dx�/\?}�w�)���]\�j�!�j��v��!������$	���FH��v������x�w�}b�����S	���t1(��\�mA�\�G3O<���&x���sj��mk�5JCw*�He���6�3�,�}�������"��������f���W��� ��B��>&�:���]�nI��%SxOwgo�7�s�8CU���T�B�T)
qXl�Z�$*�!���H5j!�?��%-��XT�
�m���������y%���^�8�(^����5�}WU��U%!����T�
�Ra����T�!��$1Z�������g��rB�����*��ka��g�b������(�����a{]����j�!\T����e!��8A]S$.�*���sU
CsN��#���2��A�^kN�) ���a�����@�Jx]�M�����B����o���V�H;Z�^mP�7�Y)�T)�V�Z�Ny�����N��W����?=��}����S��>{�I���������M9W�<}�!k������p�kY8CT�A�T� �V��*���(�>����\��+�+��#i���a����)��z,`8r�>�:�����2s�s�<�>��Y,�����j�RkRp�kd37k0�n��3��H]�Lz��d��z5%��c��=7|��;mt�.~�����)�������/[���y����% ��C6�� ��L!��`R
�a����������!���!v�^s�[���Uv����8�=od��U�e����a����#Od�+H��p�C�O�����:k��+6A�bCZ�!���Z����4����UH'\���U����hV��_H
M�s����=4�aG��9�[�o��'�<�x�Uf����m�6cyF���[���Dn�^v���_QK�A���Q����������Mj��-���^��0J�������=��B�{ ���8m��Tj�*9E���N-�G���O[1���To���{����Z�,nGM�q�r;b��$^j�Dy[s
ue����\G�����GViFW���kVs6"��D`
=�����.T�\��$e�����xU
R@C���o�pb����H0������U��u57�~��t3��j��+��+p;��P�'Z�S9�>:9p�Lk/J����2�>A���^��5,��4hH�����*�GrI_+�t�@�<�N�5�Z�:����������I7�����{�ys���x68D����<���4��k��&�"��>����\&lY��w�q�x�o�{��h��YF�1&I�{���%�����N��3���c�es����fQ�#�% �WMz�b�w�o�>H�>	5 #�Y�B�����0����UCv�H;u`a>����sg������w��~��	k�6=��G���m0��;z3�u~����Ho�����\T4���0������Rp��T��*��D���Y>�z>zk�����84t�-O;9�\9p�~�7t����vr�I��z9��L���5qU������D1�gf�Y2CUH$*��6��!���!���d7U��vq����Fv���G�6�=�����8B��37a,��2��0���tN6�z�o��|"uCo*��7�����8C9�aHf�X ���x�� �P�~��y�*K?hN�S�g�]�������h��v�r�b1��G����Ww���mk�*�C6��3�P�7r��C.*�f��qT��4I���kc��10��C�Nvl��rr���>����]��T�r��9�Zy�o�vCx�JC�8B���r���P��j�2U���bC�4��m�������o�����y�7;q����'���1R�	Up�^���Wv9L_�
����_��ud�P�3V����HZ��!�Z�H]�$5Z�������>�{�ujI�/c�mw�..�����L���������Tc@��B�����G���|[up��Ri
r��1Z��r�4���� ���V��y�|O9
�����z�q<��+�2�����`���BX��+/NZv�g��q�!mX2��\T��P�j�����B�Y��X����M�}�E�\��W��r�=�������H��c�x�v�;P�����d����UR�^nU���jp�U��
�VB��R
��O���R�7�����/���������k��f*����/F4�%�oFeB���Q��2�F(��^�p���(����^�%��b����V��D�����W��{��7kM�KS�'<>������s���c7g����n�����R����TQ��Kl�w_T��\z"S�\!�GN�#��y�8"+�����<�_}^����$ ������h���9n�����!���!�p��>��X�wGe�v>���{��i_q�5����v�G�l���Na�G/��U�8�{s���E���D),��5+����o��k��/@���z�g.�B�pn��tb�r$���[p]��������:�G�KX����������9�?o���b���]�vB�H�/}*�MF+����cg�N9UL��u�"��h����-���mv�����2�lXL���v�T����o�%��`���2Y�{��
�MDn>WID�g;J��]�sh�������A��+;)uN�t\u��{|��p=��8��W\.������M��{�Ho���M!����Z�o*��1Z��aH[[!��`#����p����4����7�
�*��u����	�2~�ZV�A����P���~w&���CHZ��!�uI�/*��kV$7qXi�S�3�� Yr���_�'����]���'�`�����R~��� \���	���W��y��=���$6�"A�V��V��T6CuX$�� �P���>��@�&"z+�������{�V�O���������Y���2���-�����{�[����&��k'$.��sU�!�j�uX$6��d�5D�j�8C\�C��ty�����>�;�����IF��Or��3k�1K���UE���EV�[������}C���|��`���CH]�N���qSHod�2���
����|�����������v�o,2�r�R����]-��5S��u�WU)��7���� |%%IHo6�Ri
��JB�T�.�Y0���!y�B�����w�2�=�+���3�Etp�4�g}izc���
n�j��i��A���>�]Y���$�a���kP8C7
 �����!�����0�����(�9�G|�&�-�G)>�?�� ���d&`�AC�i�>�.���T����!�uI�7V��
��RR�T0���T�m�����6�g�9����a�����nK���A��y��EgW~C�����
B���[�C$5T���� �dH<U�C(���[�wS����r�����Q����;Lrw�[N�������*���i
����+��Ho*�dT�Y0��+���5A!���n@�!�W�M��_�?S���~�V����G7���(�O,n��/�{�w��|(7�[�*��Q�`u��&�st'�f>���Y ����{��.Njx�o��c��Q8y���H�5�
��q9�9��Z���3��
�]{���]��P�X$������z��&�U}_{�@Ab��=z��r}FD�U������G��00}CywkS��>O�f���w�����u���Y������9�;���g�	�������->���{�����K�5�4�������5��3�vQ(\�����V�!������[�X6vi�=���w/��9W{y�w!o��J�=:k(��Hy5~�l�+p^�nOS)u�U,��^��v�:�v��I��A�����Y#�6�8��@�GB����r<����j;a��K:�2���X���_>�3��\f�^��9rb���gIQOY:���k�����oj��R�P|?{c�S��7s^wIowH���IF�R�����m�G��q
�M��rC5X$6�]!ujC�2��m�g<U%���$3�T��r�>��:2�a�_
�--���
��7y>K�����<B�%
�������=�H^5l���$.�N\ViqSHUkHe��ge�A����3��~���L��/�����vF���W���*�)��8K�������A�����] ��
!��H6�� �bCx��-R$uI����c������s����^�~9j��e�@����������R{J2&����}��������3�X���!ymaH;�a��f�����2��$�"B�U
C$�u����������w����V=����V�o3���>e��������������!���C7-�!���2��C.[0��i�n���`p�:M���]���w^��=G�hx���s:�Q 1��WK��y[J`1A{h��$�����v�8C.UH]Z�kZ�W
!����D��z,3���2����>��;l���� �����g���r�zm@�n��������j�
��a�Wg*�Y
�	�ZB����T0�R�> |;����u�:���4+r���~48p����g�V�l_TGI���X�Yzv�1�4��1�I�#��Os�jR�xCnm@�n�0�E��\�!������=�V(��g��Ri-�3T�'�7�}d��	y�������=���x���^��XB�dHe�M!�����T����8AqRC�i���B��JC��?~�x�~�����$���j�z�(f��s�Z�'�q�P�n���f�vG����A�.5!���akRrA��!w*���`��*������5g�|7��v�B���e�*���YR�������?f�g�Zs���6�F�K���U��1�f��gh79G���Wu7�!ro���bV��j����BS����}�Z�q���U��9Z��������b]iX����c��m�z�����A���z<��.�����5��>����C����y@,��������bi7\5��%}��v��z��v��Dt�=�U|�m@yn����s9z=�WE"&�b�=��D{�������������1�?>����(]��&>�(���.����f��.8:�Y�H��4/��M�b=�����R#�F^:6�[~��T��5��R��G�/3w�Ds���,����1�3I�2�b�a��&,��n����Rlx��y�#h��\��w+�����d����U����� �\��Y��-b������a�b����dw+����q�-U-5N5-��T��m^vU7���/�Nz�{����\7;]�s}��B~��4#�SWg'�Y���]�z���w4���R�T�C7�C6��H]�#!�PH7*�!���W�y1<xW�8�]��/M���!tI
Y���mgq��Di�K��1��zy�]Y�.��p�sj��Z�� �VAr�i
�P��Xd�6�A��v�e����kO��]�����n�-�����?�2��U)���O�����A����uI�3�VRr�����T0��S�.�����>T��u�S�qt�������p�6{1!{_�}������
�{3��y��<�G����Z�n����Xp��T��RYT�m�&�U
 ��
C��>�{��~]��W{WsKM&�I�����Uj��s����^g^x������yj�d6����6B�`��j�d+VB�U
 ���
��������wu��S�W_��]1f����b��f��u������_`=m]��P�8\I�U���)kY8Cw
!����m���+Z����!y���7�:������	�&
+�P��p���l��$�=4	�rQ�M����V�5k����/*��3��)n�a
����\��W�1�1����r�jw��l�����v`����� S�Hh�(
���a���� ��N�Z�!v��a
��rB���U�V��3��)
��k���{����������n���������j�b�=�)��l�S�D�~���CU`2�@HV�v���Z����i
��L!�U&���������J���������%�U%u�`��e����o�}��/�������s;3�� p�7T�j�An�a
�T���H]���*�!�k�k�?{�y�t���<����9��9_�;.���{��ba�a��#��^����@l��1(�[�wk�\�7�C9���Cw�~���bb�7[�
��+�#��Q
�|�_l\XD0{6���W��P����	�83f9�����"9g�W��}i�k�\p�T7i���pa;��O5n"#��W �p<b�[�{�1I���i��N���{
7=
c�;x��P��C��[������fz=�VJrh��a�^s�������:���>������<�!{��6��1T�G���9�EH/�{X[��
�.Q�\?g^��!�`�8�W
!���"���sY=1�0���KGs��Q.d�����Q�V�}��<���:���kJ������]�f���I�U��nN
$�G�snaG(������8"��p��
y�.5����`����!.�B�9���Nb)I\n�g�I+���U� QI�(�wsX��N��;��MU��=i7������W�j���	���ONN���f�~�7!�*ikXp�WVL!nU���4�qVR�T)���qI������#4��>��=S�1��	'��pP_���W^f]�L��s0V^Z\��3����C�D�G�SO~|
r6����P�1�d����/*�R��C������y~|��?yo�,��f�W��3D��[
�&	�n�&k)R/s�j��V����G�C$�oVa
r�4�n+H.*i�VRuL!v��1���<��{�����f���1��y[}9+{����Ocw�
/9~6�o�wISE��]����(|;�CHm�f���I�V��kP�����Z�!��Hm����6Fl�=)������j�l��4��^B~��=�e�z��S������	�T�C.*M!nU6C6�d�D�V����D��Y�/��BTC��q�c�;(��U�}1��AV�4�9���\q�x���Y^�������mRR��!n�a�.�������P�V� ��%$r�������aW��Z�.���������Eef�_��oF�g]��{G�|�f�nU�
���Nyj��yj��cT�Y���?O��t��O>��'���r��������"Z�W���=fR��G�
	ng?��$*������P�
Z��5B��C^��N��� �U4��:hW�������.�;z)6����7�U-�C���xTJ�n��L��9D��^IH]����Y�3r�A]Rd�����T���a����n��@�CM}�1=�}��|��6zL����o�]����������D���s�}���4���R�d�
�A �U�Ar�6CUFA��!yU)	����%�����C%��7����6����x���a^=3��k��V��jbz��q������r2OT����Y�e���6���s+�����!	�xmz&���{3����yIu
��q8JA�����I�����q��H8%�oPg������e�
gfo�����n��
.�^��V��zyN:�V��=@��A�i�|>)mF�`f����O�w���z��"
m������w8��P�f� �P�9G���+������d[��*������FWmG����l9A�X�^��^���:<S�,�c�^B��0�L�7��
e��0�.8FX�a+g%>�5%��QXrG&��Z*���e��D�s��v7��^�
��A�(X���v���F�B�}�%qJ��jL�i��)U�.�0S���xd��l�g&�,t��SjE��'y�f�*�W�`�2��RH�D�,�^���i��cel)��#Wma��oj��LT����
T�^�x�orWy���X^��+zo����sx���>���B�b�H;���5Z��.���������T�����>���]�>�����r5���Y~x8���(���:����Ff+���������w�pg,����,��Q���Y8B�T0���Hm�N�����jp���A�VC6���}q��AWNu�fL6��7
�I�-S����r�B��"���j�5�n�'����)����@H<��A�T��n+
!v��mk8B�����c��_?��7�Q�0�����t���$-`uL�u_��$"�L!�U�!j��]�!���Cx�
C6��!nU&�5X�������������vQm��q��K*�4j��\���G�d���*v�W~��Q!�*�!�Z�$+VD�qT)�VRkY��VCmk���W_Ve2��������S<D�������_�r9���_����3c%1R��o[���~�D�r�6A�H�������B���8C���u
A�T6CU`�Z��X{�7�o����������^�����
���|�b���&�H��Jw�<�u���L �U���� ������*��2���3V���T)ud��;��������W�������g'�^{�|j�1&o�%�����vWy�
��������
U�Cuj]Z��3kXp�n*M!�*����u�8��y�p���/�����J�kuNK&~�_�f�W�9YrFh|ll�x����$���a�U��W.U�Cj���a��������{��q�����
�������|���Y�6A��_i�7�<{���UF����)
�VM!�Z��n*AV���I�/-RRr�6A��>/?���K�#A���V���y���zA�����B�Q��;X#Yq���D���Y����9��[�W2k���t9>�JG�o��iD��..>�u~J��
d�<u���PQ������$��{�\{N�]��fX�M�9LF-#;�w�f9�z"8d����=��<��1���uG��*|+�#�U��Hj��������F����s�<���������+���-L�z#��_:�Z%0
�G�o,)��8��	�z<V�\���2sS������r�:3?U����6
����P��G��XEuB3w[(g&����`�M�r��j�FW.�v3�Y�%����f��^_�4���I�Q��VveD(Xv�����P�D$v��/�vqyK~q������Xg(J�)�s���<�S6�[f��V;2�j�U-��t�e��X1��k7S��o���w��%r���E�]��P	�*����~���*1;���/��vi�}5_��>��qt��(C��7�t�[�l������e��A�t�����D�]SkZ��r��n�0�����j�!��L!��u�>|����_�����������^�u�������lu)o��CV���t��O����UD�U�!��n�Ri
�Va]xCyv���Q!v��o��������hQ$Q��]"�T�|�{�������qh���F����zz.�Xa���B��$Z�[u@�
����1��j�=u������~V;�\i3���*�K�a����r��)P�^hTk^
��xE<x���|sIa�Tn�#��j�5j��`���5d��j�]�=��;�\��Yw�������D�p��:L���R���������IV�Z�f��v������T�j��Vi����{��|����=y�{��uC������������xy����bh�x> p��W�����1��n��CwU�v�
��*���^G�|�������`��eJ5�K��-���W.�
����#6���7Q�t���8����S����I]T1�f-Pr���������J���
�����c`Z����\����
�3*�6
X/�~^�$RO�m�r��r��T�j�kTV� ������M%�����:.7�,.�S���������F���(�$��a}s<�=
ow���$��)$���*��VsuR��=�����}�6�:t?��uu%�������F] � ��W��T)VF�yE��.���R�P����j�U`j��5g���|	���5���s��aj�[��oZ�����_v��U���������K����Ho�5�De��Yym�0W��+�0��I���}������aB���_a����!�-����n�f�q�0�I�Y���b�j�Z���k���,�>qL����k5��y�������0c;F`��������S�;����������h_��}I����
j/
u�����w&��}�����[���8���=�{7q<��oI+���{�b�ozg�� 
0�}U��61@^S�/{z-*�uy-:�.�zr�S��b�<�-s������_&;�8^�t�n_w;X����n����z��Y)�J)TD��
i�\|K=�p����'8XnW����E=b�����T<��[�Ih�y^��0�h����,���SG[)��-�
�K�K�v�\�h����k��u�1�X��!�i��5�)��	d�44�b�6������g��w�y�������s4�u3i���J������i]��}�];���������Xf���U
�X]�a�����]c�����W������4Y;\;��u��S��y�N�$cXE�"; �����6���Pn���V[U&��r��}�O~����3	����n�,�Z@���*��_�z��Kx�N`�����������������j��U&���T2����M}��g���k�����p��:�����C�
���5�k�P�
n��3���9�����NI���1�d��Z��j��U-�O��'�c[��xo�������KM����C}rw��4C�O(<rlif7X�����^���7Cu��[Uf�[�P�������`�����>��oY���u���X��"k������H�f����0��7�s���V���asU#�P��&��N[T+�a�j�[����P��������������L��Y��h�Xb�OX������S(������G5Rn5f�VUC���Z����$�K�!Y��W�3��p����rw�����|'�s���-F���'�E�����~�����R���`�y���j��X9�e�_g���s}�������6d���{��"����h��+���0,��o�JI�NH>e��J��Z��VmY3Z��U�~��}�5bY]��b~����O[�����o���p���a��������n_~uTwj�6��Y�b�C[V�Y9UH�����7|��{�r��������[��L
�V����86��G.��`�����cQ;�/���MnrJ+AB�V>��i�!�"�z�F���N��
 ���k�w����,#
"�?_9��9�0�
�""/��s���&�����1�w��i�k�b�B���	>��J������W{�s��QUQb�}[V"�"�����*�����9�.���+�Ut��Z1�9��"(K[�i7�|r��<���i^�����J���4*a�����7��rind����A,�C�������4�00�
�o��d���0�(��O��&�����aTV~��[�\8*��(�9��s�\P"��"�$�O��oQFD�J����d��������n{���E�En{D��Qrd���aaQDaTXT���g���I)�d,�w��fF*!��+rV9�
��������/�^�K����]�����p��=A{�Ua��8D6���r�g���t��(�����]fpR��]�DEL��XQU�������>������*���������'�x\0��"g7�N��bDD��s�������
����sx
"��^���e��:"���������"���u*\g7x�h8����M��]b�X�wQ��v8���|1L�l�;\�`��~!C���������u������������iK�N>�+��h�r��6�q�]�&�����]�*�P�A��i��>�ADDDAa}���k��gAAFQr���*�����!U7��w�~�Y��x�E����#8����������
�EV��_^w�r��^��#�z�F����9�}�u$k�������%�zX�������X�U���M�*������<�U�m$��;��Sk2��-���]����]u���v+:���6�)�"���3���Y���]"B�;u7��x���� ��������}4*,B��
"eMv����EAQ�c=r��T[n��~��������"*g{���*�"����1DU��h�����5�	�+R�2�\iTM�uGc�U�������og�����`y�
���wp:���c�
�}����/�������i��I�������*�W^�9��s�}�������E�V&�g��;t"���$*��r	QTD��]�	P�� ��s=��}�^��������0��k>��w���B1	��~�T���
���
�}�s�����P�@|>
�<��k��X����������^��+�h���m��6��'������u3C�L�>�~�����G����nhV`�����0���#��5��{V+��y��^������������EV�\�|������,9����g��a���DSw{���;,+5��{4
���<����WwUSfM+���TA����5XXDUDE��l���"�W=�U�W������os0�M�R��+���1���)f��f�Ny��
J&��j��wit��x�D����$g89n�����VH&B�
��w6����5��u�VW��{~����Y�^�f��
0��s�s����#
���_}{�t��b���"y~�w�d����*#W�u���F�HXE^����������
����\�&q���R>���~���"uA�}'��W����O�*V.d�T�esUY�qr�{^n��o��b�d�	������(WS:rVf�����u���{Oi����d��1��Yf���W���.y)��8Y#�{��������|�P�B��/���������
�
��������ry���*����}�g/�Y����������?s/�J��"�������(��Y�l�
��("��;WuUH�("�;����%�_ ����B�*Q)���`����rs���Y�a�DF>J�N3�����e��w����~�^��Qs���.D���g)�R�������
�CO�aa�M�C8\����O=�]�������3��{;9���aXEE��"��ETTAD}S-�j���0��r[�����}��Dx�g��/~68)
�(����~J����>�E_Y>��o����L�
������e�eaF�TH����gi����|�� A�	t�S�lo�;J�TQo��p<����3�D��V`�|\"5�-L�d�F�xs:��wP�|Y*��V�������U"�*��7O2�Db�2���L�k,"�+d��I�Aa&�����m�DQ��{���b�E�a*e~��_vXDPF���W��!�UEQ�,���������\e�v�r�2�5nV�l:��C|}���5~�������+��K\'&w�����eJ�'�"�;�#����,����n0���s�u�d��O�g�����M��|�P�$�GM�h��\���+0��}|Oi"��)
������|!�`U2���Ys��~�� ���
��<���5�DXQTa�DaM}y�5(�����,<�2�]���4AQQg�L�D������[M�{a<i����+������
�����v;A��s�f��z���������d�>���=:X��z��d�HN�K�t�F�N�l	�T�/����B
 �0�7��S���U���u�)EUK��]�{80����0���={���QTQU�}����L��XI��}�WjjUUEam�N
L����2Q�l�����_v\�zp��o��vUTD�[����_E��Z��.��W�����@�L����0�",����/������t��T���"��P�@"��'so����v����}�EUsj�IUV�V�|�{�9�����Rlj�N����vS�
�,(��r��pzL��g���{��]w{���w���TXXQUUaF<���uB�__m����?�
��w�
(W������{~�3��f��W�}���[����E`EUaG�^���o2:�F<q������� ���*���"0�S)D��D���������G4:Y9gP��Z-�{�:�����po��$�1#����f��I��"�
��w��&�{G�����c�T|hj���P���~w�����'�;�6nUQQU?}�'<zx����
-�Y�=�S�QEQa����|�b"���B"?|;n�8fU�EXn��u��*������W�H(�"��9;S�cDTXUL��.�S-����bQXDDTDF@d�.�~�l�����Zni�(R�
,"+�L��*rf��}��k}^}��f��X�������������|���[�9�2���w{W���
#"/�^��9����3��{��|����vw�������,""�?C�
i��2=�Yg�����_L)b��
����'7�^�x����{O{������LQ���"��((������
�4r�s�M�N>�\�vN��{���
0����O�v���l:�a���y�j�j�]kJo$��(��+$+��%W{�����m
%�I���u�W����*���)}8y�����%[�������[�3!���^�^�5a2�MT���NTs�����-�����p����J��nGL������c���K��#E�6p���vw��A���/�������){��E9�Na��8�"*���������`AAU���~�{����,
���}f���"0���o����7 ��"#��s~����M��������QQX�����d����J��((������U�$#�����<���������]��FQ�TU�HF lT���Gz!� ;2�%��Ku���QF!aA�S�2^Oo���}���g=�n���U���a�a����ky�u�L��I�UF�D���
0�*B��������8��x�3�}��+���`����
0(�0��Gy���k���q�;=���W�O�~��M��9��QE��=���vk�r'��*s����bE�w}���#{�/������MrN2�\8�sN�J���0"#0��+��y��f���1.�:���l���N���(R� "����;�����}�2��Q��WpYE�p�e��&�������ij&���a^�u�HF1yE��\�:���.�:���/ti���>�{���=�\���TR��*#S�o�
#*
��*{����0�1
+��������UAL�m�����!VQ&�����5EG�l��s*����(�����<���w"aaa���5�;���",*��}i�%�������0B�jy���>�����d"����0����m���S�u�����h�<#PDXD���������^���Ne����{��x{�\��z)VETaETaT`�{y�|���^�O{��%�����/ny%�6PE�h}C��N�>u��N(���d����
�0����t�/��M���2���1��&��u�|�1
��*�'=�w+,���Y�z�o2�����6N"�*(���C���k�|O8H�]���O����i6�eP�E��QUETB�s��w��q�}�WW-/&s����=����XQFQ��Ly�����Y������MrB[���Q�3t�0��Hl�1�}=�����@�\#X{6������e��&L�Pl�3Sy-��B��s@��rv�
���|�zA�C���|�0��
o�Y�����E�`x������E�a��.�w����	)O*~VT`T}���_7.���������]���mY+*0��������iK
���|���������UaaAUXO����v��eYy�.���b>����j�*��A�aPtv}�s�\�O;����oy5=+��a"�**��(�N\�C��4�6\���������{�"�(�"�"0sw�=����U:�}��6��}�9�/��#�����>�e-���,GY�DK��y���|�����K9����9�]��.d��i��{}��v��(��(�
+�����4��}��v������dm
�*������*!Y�Vs�=9�z���]_k���\���UXUaEUZn�x������|o���=�s^�Y�[T{���Ua/8]-p�����H�s��h��2n�����(_�[�b�6s�)Yb�~<���Ad�^�����C�,l�����z�&6��-r�t=)
T�tI����|6���F���u���y�9�{����
��R}�}��#a!2��'����,+0}���UHX0����K��2UH�o����u�9�,*"�)��U}��X��1
�/N��kR�(��C	�S��C�M,C��
PB�������J���4TY|�������QQ����QE�(
��u:XXzn])2@���W���DAEPUEUE�J�@��~���nAw�u��m�Pds�$E!EU�XVq��r�����7��:W,�����`�B���*����|��6����m��;�w��O�y���w^b, �(�"������rso��I[<���[����YS���*�B	'�������*]��������
��"�"�$*�
���{o[��]������rte����[|��qAPTaD_|�4nE�����'�u��~���U���o����� ��"��(�P���U����sC�Gm#���ku���,��O�Z�6�.teZ�19%A��x]��]��ct#���Io��}�?r9Q$�0����c���'�v���
(0�o�����v��7w&�{\�(���f�[����`Vp��-k~��DXA;8�k��

�2t��pQ�Fw��(�
����zN�n�|���OmT"���
 ��(>����kkV�,�ic�3���Og9���"�@��l^]�wb��x������8+�Ba��WF�

�*������y�+.j�I��+%�o^�������(z0��0��
0����{k�>j}�p����1��aQ@F�����W�\���vnfONen��L���|��c�*0�
��)9uEM��������l�������svl�DFUb�~�����l�]��������v��gcEHV�B����+U��M6�>#�.�$��|��c|�w�u�AETQE@(C���Z^���m���J�Q+w��y���00�#����H�y�K�0'�1�p��w�X��T��6_xF�P��8qkT*���jXj�X��m���R�c 8���Q/bh]���,�Y	%>�/|��k�CY�M�����>�h�x�a/J��*,!����/}q"*�"0,5����+�dabD�������{rma DEW�>�����-R���7��
����~��~��{�(��
(-w������k-EUQ�������Z$HVVU��t�N���;Q�:�gG��~
#��",(+��S�[73X)����R)�����]z���B"*�0�*����VWf��e����M�c*����3N�����
����(���=WO*������v����*������NS��DTa�a!QC�A���5N�s�b�y ���h��)�Daa�Q!Q&w�hV��O	4��Sh��+�U_U
��(,*�5����|��^s�5�^9u���o�o�����
+
(PU*
�o^���7X��Y��kU|���EaaXUE�XG���g���t���Y���������r	!�V`DDHWp�������J�4����h��WQC�5������p�
���;y�N�h�#�X��H}p�N���byU�Q��6����U���l�d\v��Qw���-4=�����]%f^%,�����{oy��*�]���~��������D�k�J���0������� ?_�]�+VQ�V!FD���^s������(�
����S�{����*!EFF�9�8�*�����
���*���:�S��,*����
�%xr���[�VVbE@>�%��|{���
��V��y��Z@���s��{5�N�p��;�'`D�����H�,U��c�<���&%w�Z���pP���������5�5��\��.�C��Q7-�H��*�����&����������X�fl1�D����^������piU�����)�����U����P�^u��������b�w��q�����s��y�99>���w������|Num��mM��.$�H���C������	�>��o'	����XxV��xJ�����tz�����\���47,��u@�&��Q:����w��v���)6�Rd*�2U�D7�a5U\�B��nA������E]b;�FLS�2����i`;b����vr�<y���2����<�,����id��_2�+����1<R�eq�#��KI[E	�������7��L���*4�����t#�w'Wg�%�IX0-�grZ��P���
xn�:�{CX���G�L���w����N�c�L+�z�h*7�UzOv���������� {O;T5������|2j�C
N�'�q��V���n�BEX������&0���wY��C�9�P���������z�0��w2&��`�V�D�����o��up��g,��9��2��VgmA�6�g�����X���
����+_fqOpG�|�g^1�/6�`h���M��B�;Q���^H7�����)UN�<��������U9���`�%��fKs����vC�!b)�$/Gm��N�-8)n>���t��vS�s{ta��0sbk�\i
"��n�����KJEp[��
�:a�5���������innR����������|-[���
S����*E�/RjI���
��b����M���nf�f�7^_��������f��F����n��CvN�%:���\��P�~��}�+.on����gd)�6�7p�����"�wa���������@��;�XvM������������,�>��N+��glPO.�9YA�z;s���&�,�%<��'S)������P���N]X�j��
�O�{�����nw"�k�D�9:���w�j��@V����(�a�����iP+u�V�`�P�4=��]���X�/2�Y@�kM��6�w�����375,zoW4.1�09`�R�|���yR��'�z����e�_Q�z���-u�����c�E�b���uK���9�d�b����-�aH�%���1%����wJ@h��w�A�<��U�L���Rz�A��c���	;�e��En�K�	�����%�;��];jNC/���`�q�y��(���8����r���"���h��v{�A����/vo[lca�r=<�������cJ�N�uvx#('�y���H�����+n�9E}���JP!��Z��m=���>�3��y����L�8,�����.������@��������x%(1++8W�!hV����Dio1H[F�CV�9Y��������d�������n^���Z���[�)[�&g�I�6\Ya��ET���Z�O����o���K<�d2�Z��dn�X8
w9z��##��x���Q��v5�R������d�w*�z���^HE�����e���ZY'��e|�z��(�����]���k�4/R�J.]9�B�|����9�gTf�L�aY�ifI.��N:����������|{M;�\��,�'��M�������w`i�#���P���(R/��4���s]�\(k����V<WVYE��X�7��GK1����g�#/���-m��P�Fm�����.�k�J!:4��r
4mA��=V�;Ey
�����+��81�����f���=�����Q�\[�B��W8V�V�N�����*���e��1�6��nu4��v�R����X{�{�;V��x����B�8+�)b�X��[�zh�o4e�N����,�bf��or�L)��Hf8���]��g�6���hb��p���Z�e�Ir�>�����M�7�l����*<Zx������� a{��u�&t}�7��U�s�wn5�%n�A��������]�&:i��S�S�]�TV�y�A��aOS��������K�>�]�����������nii�;���3����(��n!O+����7����f��K��{�N`����h�������^V5�9��w� ��/d��@GVO��v�z��]����Z�T��G����}�zV��������]C�|>��]�������U�i�~|>i���*��y3nTz=��c�_\�����D1���kz�X��U|�{-gb�����
D{�^ap�� �\���������	�TJ���dk5�T�)��}����u)G+=q�y� N�o��bt��;����hLT��E�cv]��rb��Sz����[(�<S��G��8�����0v�u�Y��l�-Ls����T]��Ov��Q�q����J�:� U+r��TV�1a�/��l����UZ�qS'6����CF�>V=�o�o>�z�s�]{iZB�n�[�nG�e��{�X�%�E����'(�I�y�
�����5�����;�(�F��G%{+�q �������7�u�����m�@�j��U.�He�`�jM�VV������s���gf�.m��8�;�����'�.����E�@�����W��(����mY7Z��j��VsU`��b*���pK�\�������Ev����.�*�,��p�_��mm]�&$�w<����;����VULuX9j�\�Y��75XZ��������0��^b��f=�����v���I��dP�U��t�0
���j��*I�jH>�+%�T�v�����T��!n��~��WaBb��9
�:�V�Y����e�k7�0_��X�[��nJ`WS�_zA�R������9�P�j��j��Xn�I����>�=��~�{K���o]v������iu�?�����r��~_k����-U'5�Z�]�����v���fc�y��m������|��_=/��
�����+0A��w�e�!:$���g?.�l�~�{�L�X����B��1U��>�vO��l�v�S����{��H��pjk=#�k�c��^8A���A�\�����mP�U��P��b�5Y���q�nI�|?f�?���n��i;Y?8n�O�_�r�jY�G�6�,NX���gy���z�����
��cT�mP���UP8�P�j�9��7m����&������]�A�����$y��i7��	�9�!�:��&s�}��*�}�J�d-U#�S1�2�X.���@q�m;v��<�����A{5�~�
����ks�)���Zk���fz�������	��;��/l��{�����z��vlv)�������X��5L�����_�����P����b����&��L����l�!�q�����p���������!��+�������������o���\��>��"<r�8�����:�=�&
��<7���TG�������[�B;�+�'>�����km�{U��D}��Ri�q�����H�d;Y�M)�[�\DC�M5�'��[u]�54s'�P��&f�z���qF=c�/.�_���YL�J�os��q2�:Wh�X�+����K��*����tL"�{�i�,7r6�����]���Jv�q�f-�u�	X�J���k����)u�E��=e�����I�[=�U����nP�%�U���b�	�G���X��N���Z��1s��n.������r�qs��o���&a�kr��^��&�O*�&�mg�{����Cj�+U&�W1V.�v�5�@v�%��;��O3�g9����_�$�Z�|���T���1��w�s��-�9��|���c�Q�jI9$D���IuT[Wj�7UM�������F�~��������~���������{�}�]R����In����jH�r�a��&b�US5����O����X��S��\>����R�IZ������(�[��gWp;-cCv�G����v�8����%���j��j`d��j���&!�NU��h��G�%���]Zx5r�cX��������{V��o]*i.������q��jI����U
�����T5j�*�����a��9���jr.k1���������~�u��T���Vg
��$�$�V��eT��.Ud�Uf����(�LE������-�����j�S��f8T��	[2������*8/4�[�Yq�����U��T��]�Nm�%UC7j��b�d����g���������diNoz�n���F���q�j����6���AK�[��$�4�����������m�mY���~�����jz���z�j��(,����{\�����f�z����~���v�
�Rj�uYmVn���1���9>'�����^?Jv�_����}����E����������!�LO:9v��d��P�zoq����}r|!uH]U�sj�r�L�^b���\�t�{N�|7uB�
�����==�������A]}19����~ry�[��f�Jq^����j"�����K.��L2j{��/��LX�W\���$��|��j��i�fk"�W[��SRtMvD��n"szi��L���J#���,�M��ln�;�]����z��:�P�k��/���K����y�Z�q��N�[�P,Fr�c���w-����m{��5�j�k>�|1�F��������}��3�����Q;�������b�us���k�^4��j�!�r�����]iz�a����;����"��,�#�'u��c�5,J�����:%[���d�<�Z��Y���C�n&:;}n���&��������i����>��w6����*�>�I�n��$6ju���o�����=����rf�J�v���nW&��R����Q���/o�L������vR���Y�jFT~�X<��[8kFn�/#�����#j�GR��G��;xa��J�d�G�d�zs�cbO�����~���������#�������6�����]u&���9>�������\r�����x"�g�Y����7��W��j&���.����t*�;���|)' ��$7 ��U`���UC��`�V}�{�=��)�=��tFe����.-un��]�C��$�~9��D�4������|0�$�U
�������mY.Ud��$�@�&�������y+�4z5����4p��?�������~&���y����Vj�������C�D�z��b(��o��m�Iv����������k�&�����h��]gb��o9�|�Z��V�����VM�V��6��U;�s�7��y����7�z�E������v�����"m�N����Z����bL����6���Y+U�3j�����a��"��'s6>���������u�}�~�=��e��wA��������`h�BF�{�{8�����N*���C.��]T��P�Vqj�m���'��O9���hG�E�mv��Y��YI��/�^��{�2/�mTx4C�inN�/!=�����}��
�Hn*������j�9�'�� ��J��~��Y���[����L��>c������A��Y�0�B��w�����9j�v�j��Y1�`]��1�
�X5C������9�"�{�����;x�S�W}J��{K�?r��S���w�eI>�$��I$�*���3j���7U��Xu�P��Q�����j���!w���
��'RR�����r�I��X��|��������(���������j=|uGvc�2�����w��+Od;Cs�e���6������-e5)�E[�w6�f�1=Oo�C\��^�����i��&.R�V��/�?�"=�WV_�&-�����_��5����|>[�]��9��)�b��
�Dy�����u-���"��'�I�����}��f���
DQ<`��������f��d������r���z_n���=+x\a��vM�I`��K>�[�������\��:z��=k����Y�M������b���1�����r�&��y�a��F��rk����u��������W3y������f��%��-�
��R�L�����(����R�G*���xs��C�lu=#�2�f9n`a��0����o2�"_Z���(���!C�:����n�����;�i�;��]v6q�_��w�*������Uq��a,�"T��N��2���[�����\{�}guB��9�Rj�nUI�T�j��'�*��9��
���S��W/��cu��
o��~g
�i�v0sz�_]�yNk����e�I���*��I�j��U��V��Uf{���=������c�K�1W���Y$���&x
�u�����]�9��������$����.�Cj�k����eU�7U�O����y�]n?�_U����R&������&p�����H
Rn=��s�y�o�|�}�^��P�U��f��nj�3Z���&�����{�~�u/��y���[H��	����������gP��.���{�U�u�� ����T9��.��v��U�Xm�f�x�u���{zF|l/�����������{���t4G[�6��U�y�H�~�9�P�mP3Z�\U+���S�*���Y&*�&=�o���~?S�1�
;��'&~F�e�/k`l�������0s��P��67�I�(�D��)$�+�B�����U�Xr���w�N���F��]X�v�f�������&���������7�w��f9�kf�����Vj�����sU��T1�'�G'�F��~���U�.��\����T��D9Lg���H~����M����/���Y��gI|~�9UC��TZ����r���������>����}�y�\r:�{��(����������2l�:1bfN���5���8��vg��B�����fmY��d�Z�7j�UH��5�����:�f�?7�����Bl��dPOr��o����J�Fo��-N���f���c��Nu���.��2`s�2���Y�MQ�&p�������AQ5�I��)����h�������=�c�9�����^~��EW��5K !���,3�U��j����#��'~�����
�d�f,g����B������L���<���v�dT=���^�G��9���1�*�q���4�BS�{�8��G�t�uFw)��W�b��`�q��D1�+�>PS����a�Kd�z��;�b������S���l7�����7���������(��c:�T�|�}eR�\�B��2��}�gU�Q���d����	Y{$�\7������4��V9�H��	vg��t�.���ai�y��BsP����v�4����C�n���u9�6V�y.`���x�� �*��kN���p������8��Cs/Y�/*A)��k_�H+��O?��6E�P]��\<Ty�Gw~�_6��3U�.��������U7N��}�}����FU�?���Q���4o��'�T�p�����W{��\�s��t�Y��36���I��3uXsmY�j���>���>����������[�-���~��-���i����o)��lw�V�5�O/�q��(�4��mY35Rc���X[����jx�%~4zS�?[�)�=o����U�+�?\o/�����dj�������1j��VL�XsmR.������#Z�WU%}��������}������yy�����W_��^=�1U�In�\��I��;uF���TnA�mRs6���T�T�Vo__�>���I�#�st����M�s���Tm�'�oJw����$����������w�7������a�j��P�U�Z�r����8�Gg������}���v�Z�?O7yQXH�h?�Lw^��~��+������KqiF���G>�I$RR2[���j��P�5I��5T
I'���T�O�W����2�R�b�K�����p;_����x)�����z�n�w���e�R9j�j���%�T�v�v����9��y~�������O����n��(u)�f��k��o���6�gv����@	NO�-H���.���U���6���U+�sL��b�x�����C���^I�������!h�������6(y��~{�q��n������sv�[V
�CsU��V@~���y���<���?^�e���np+cS�����8�,�t�����y�
�rz���������\���A�\��E����6��29uK�����>��U�X����G��T��o�MY�Y���!)���$����/0h%�*�hmJ��6���w3�vF���!deap����:�����,�G��ndKb�����'����=J����U��a�����f����qu�iG��iY�=��8r����7�49����M/DG�c"����]O��%����N����<�<�������;;�kgZ�
�e�	�A��G������QT�L����r�Fq�{9��N��Xu��eE�u']��IG���wXd�2��K����A�|D}�����T�w�7#���E[��8�
T:��HR��FE��,W�+��w�.�e������x����������3�{E���!x��U���3�����)B��v���Z�7{)�"'X%����33q<�����;��<�a���0��'%k�{<����6��m�m�I����Pj��l�z��}�q�89T����^�������,S�����B~�Q�ta��M�M�������j��T�����.mY�T�uYV�U�Nu������z�"��K�8�*L�����S/��w�����D2�����0��)��M���=>�j���U��Q�T�j����$
� r���r�?-Yb�o+H��d}n��pT�����5��c��u����9����C�VGmX5�6�Y�j��XnU@��"�}��/H����CQ�=u���[o�P�`���������g^���z}�>	� -���f�Y�U!�U�1V�T��M|����������f�O#v`���������~�H�J]���}��r�[U��f���Vr�Pj�*HI��������eI��mgZ�������F;Tu�����!*p���'u�v�{��9�VL��7UA��8���VnmRj�9U:�wy��c�o�}�P���I�=����E��-��l�<)��}z������7��������m�
v��U
����I�t���p����������?o���!�����3�vQW��=s�_j��P��~}
���X-V�Ywj��VLuXsuPv�{�|~���w��������)L;E�7���{	(u��>��n{��|�wj��U��Y.��2���j�+VUzz��������8?����������}3z��%b�v<���h��gGm��
�q*��f�N�z����{	����.�f�6$������U���&S�|)�V�.���{R�{�T��u6����s�i���=��J-���wgv����p��O��������9��/8����M�A�z�����DfW=+;L�F��f8����*�Tz#�X5�O3t���uZ����p\f�,�����3j"#����n�g_�G�x�U
C��a��T^�O��.�)�>���T^����O��aS:�]�2y^��1��+F����fGVh����-��QL���mLJI�F��k&���]��1f�R�]�$�X���I}|~�n�q�5;�^db��xZ��=*����#C�6o��R���Z��]�5���l��VA�m���)�R��
9���B�����9+��w��^��9N�l	Xe�Ny�>�-D�i|w3Gv;��]%�+�����^��?��F��n���"j^;�P�U�e�6����f:���&����X�Vxx�������_:����5~��>#�_���W���?l����B���B�������RI�Q�RI�J�m��q�j�Uym^_��>��Y�o��}QI
�/�������aGe��~b�[���k�����S"_�J�����T35Rr��7Uj�uT2���>�������G�������d��y]��G�D�3h����q��
����s��u��u�9Uv����.mP��e��e�I�T����^����nS^��p����y��^�����9���G=��Ww��I��Rr�rN;T3����wUv������������D�g�~�v�+2���9���o77���m��Yc��J;���o�����<%����uXq�`�T��n*����������s�?}��N8&z��qGr�,��]^,����O��<�z��V����3uRsj�.���j�uT7Ug;���>38����������o��n
1������E=�X-c
]u_��s�bk2�Y]VsuQ�P��@kT��H��>S�#V?g��������k��Cm~��3�7�~@s��.��;�`�j����������
��.5a����k��kVGmYmV�?g���T��;;�~�e�������)��O�;L��,��d�F��R���V{@)�>���9uXf��j�j����j��� ��h��[��0?-�xU$U\}�K3B��M���s�R�&\�������^K��:���rw����|����3/�������f��f
��r3�x&�i�
�����*���/y������j�9)�����S�e��A��Ir�n�_����L���m=m�;t��}���F5l��y�n��o�j�Q=K��}����5���w9��*�mz=�IY&�J�nv:#��y}j��0���y���p���lF��e���|��}�o��W�8|>��'g���w���4-��7�jM�g-�CX7^��t};!\4�
�IV��n�i���u�s3��U��*4-�������L5y��uL�'9�g�`�[��$P�XT)^��
������1����o��a��e,>�}�5]�y�h�S{�2���+�?3k��fm!|��Q��5gx�������t��?E��A���k�52��?!�Z�WT��Z9%V����������~;);��/UL�Q��*�����=���Q.��K������	�O�nO�i�r�&�T�U�3j��T��O�<���Y�?<����t����|����1�G�����ETY}�`.W]��Y��u
Uf��3mP�5e�X�W��7'�' 	�=�W`����������qs�~w�%�m�w��B��g`>�D�6e����2I��9�9�E'.��n������U�O��pf��xTo���>/9��C��L��5k�W1[��}��cGAe���?��.�"���I�VN6�qU���O����D�n���B��~y?h��f�=�y��s��j_�\#�������}zz�S>��]]�
��uR��&��1j�wj�]VcU����/���'�[������I�����5LE�����������5A+~���E$����Y��7U5���]�m�d%<�1!����e��zr�aN��"���������0*��3�ssHN��}��-Vn����7���#Uci�.9>�����1Vw����R;�����c��M�,���:\��s�k�}q0�����	R4:�O;�I�1��kT����%����a�T��0�X.�����0t�-�
&������i�����+���nkXsu�sD�;����C5V]�d����F�����m��Y7�����_g\���u�M�K��D�|���+5��te^T/�m���r�������|b�������mP��L�VmRy���_�\_L�)���{Ye����E�K�; ���j��<��[am^B�[�N�y1��`d���.�(Q�}�3�l\5�!�����\|��f��*4B��7�u%������{(����a[by��=�\�?{^�9���:�w;�>48n�������Y{�����s�Y�{3�������m��u�W�9W�w�x�s��*T����@G�}]��
��;\{��}�.��*�z#��a��H�c	����Z�H:~[:r��z=�0`_�?���|9e~
������"�mY	�N��eN��Ln�e��T��Hp]f�k�,�:�4���������)������������FDW{�
�|����1�4;F����fsK4@�������Z��Aq����L���`�n�����k��m�<{�3^�[@S�3�0nVgB���ll���Yy�B
w�;AO����t^���2�y��n���q�������]o��W��d{[�(B����$�@���UKuQ���R��U#��zz��(��^�;�\s����U�n��V��8HF�nkgmE�J��/���Uj��f�IwU
��qN[�|�rwx��5S�=y�{F�����9�3�@t�O�p�Hk����n�R����'Z�L�?g�*H>�9>7!f����j�kT����`����o+�����N<�b�Oui��?c��p,E�=*���v��
�j�������ar�:�9j���s5Yv�[V�P7����w�_�������M�'����������8i���V�62k���a�?Z��(��5]�W]TkVf�Rs5Xc��jr|!�����E���k!���f�S��>? ���}Z���LU���faY?j~5�Nf�M�Re�Im�"�ff�>7$�$�����~^Wz���m��<O{�t������-�c���V�Q����?u�|��S~��`���U���9Z�cU��Yn�E�c�Y6�����s��@lo�K�=����B#DZ��nzo�X�) �V���+Mv,����Y�T3v��U
�Q�YZ��U��$	�'���q?�S�>nJ��7�
����������?vVJ���(��1
���xs�~��p��2�B�T9�Xq�d�j�uX���Xu���|����?j����C��^��������j����Si`�v5��x�����
���sv��R���j��5j��~�H��t����6<���<^��P�H�no�_��^�doF��2��nzD�`��oFl�7X�����}2Q��N{�j�����Hs7���w�N���T�}Gy].��6�*���n�vE��Roc����q�#sl'ph=8w������Ug��^;\2��p��P�>�x��m7�U�}m��Z�n���@^�{�&���;����O�tV�����O}��}K�D)�J|}�r��z=�Mp�����6%{������; k�>U�����T*����"�]z�}[�m���+{��q����w~��s�a��W�����"�1�d���:�^�(a�R{�Sw�Va�Q�V9S����������.��N{��SRJ������U�nR.����jsz����TB\�/-l����y���'��'Et��'#��uS�J��n
��c0S����]q{��������
�WC�o~�5�w0�S���F�����d���	����6�uPr��P�U���rA"r��"�����,�Y������F������ �_��2������\��������Z�d��%U`���Y2�����-�F��|��E��]���+7���<vV���g?h�����%h���j�(]:O�]�M}vIC���T��g�
Ud�j���\}�I/{���]H���l�#V����30A%����H6�X�e���C��v�q�������Z��T1���U'������~��=���C��w����Wa�2���s���}����G)����SN@*I'�� T3-S��Ua�Vf��~|t�����~����#_d��M(�������c:���!)�aB��u�&��]Q'�9j��VK��9v��mP����I�U������>v��n�n+��r�mp?s\e�����t��&��A7v+~���u�
�P�VKmRk�a�V��kV*��9�{�����������������g��
N���h�F7��)����a�?u�|L��r|*7'�n���j���9��2����_{���!����b��g-��3��N�^����g�����x���lT������`���UL��.�B����nG����Z�)�����o��T�����~�[�	�H~(�����yM�����w;f=�o��d�*��T��B�TsU��T�j��U����Uw���������C����7/F�������=V�nd����~�NN�:x]����S$S&����;����a��}5yW���y��(�#�0����VB��0�#�wr������U��w�N��"����/9�,FW/�}�/:"(�����	;��ga`UU��2j�j��r�-F"of������_7Fr��V�N�9�,S�Va����2�_Y����5y�y'�i���-�����[;�x���4r�=��5������9��$��k�U��N8>��O�P������|3�n�l���*g���9?W)�TUD�������
���1�&�k����(�,#������*(��r����������
*��c����,0��������{��e�k�<���kQ�C������*�y���z'�S����u�|r���X�f�J:���N�;��;u v8���y;�b�e^v��n�w{��d����b,*�EO����D���}f�\��*+���-�����""2�5�w�� FH��WRm���B�0� ����w���x�aDE���rcFUPX�O}\���U$��g��������b���il���ox�+�t0�b��~�>���������;x�<�V����7�N61�����[�?M"o2{���c��8�g4Z.��?o3k�Gz�i�z��g�ua�W������(�a�7���m���EXE_����qXF������EQF�O����_������(����z���DHUaaUX��s��������2���Ukn�wL�L|^�������*����f�`��z������/�k�=��2"��b��m#��:K|�^��r����~-_{������)�<36�c�����t������DJ��*��������
(0����7s���sx�#
�����T<����aaXDDQQ��7*��e �B'9�t���!aH�������<�aXUE_S������(��(Qj0BC�%'��U/m���l���L�!�M�|M����1��U'�{j�oY��v����J��F�c��������^y�[���������e��N����?]w�w~��y��a�Aa=[^��I��aE;����w6"��9���EDAXay��W��}���PDQTU'����M� ���	��W�#���
�n&^�Z��B�"�N��&��L@U�o�������!4,kXHC��nC���Oh��t�*wb2�����~Y-��S�������*�?��&w�����;��d{�'����L�7��m�v�F%�J-�����
�1�N�r���EFRq��ep��:�����w^���QQE&�5�Vac/�������TVDFj��9���AUa!���Vz�sf�Oh�>����v�kl;�*�6�j�h���s6��ic<1$���[~�g�s��<��sn�S��7�����6=taS�v��d��pZe%]��F�����0�%�x?|sf}���0�*/~�+����*���	q>����0Q`U��T��e�����PPA\����=�_p�UEX9���_��B��0��bY��}���yFQQ+~����aU	$�`��_[K�v�N��6�c�i!:��h�P���
b���k;���"��4���&�}I�����Q\���1�y��9p3[��g���w]�B�m�4�rV/�z����h��`6����}��
�����j��`UA`S�*���`�"��(3��k/�+�`�������L���O��HE���'�H��"�*�������
�
�����N�yU�S("�(�I93�9q��e����/+�U�����wO,u��,K$W�j�@�������
��{���^,�H�����6����3��4�x+0e�:'��p�[������W����G��-���T(DAOV���'�s{��0(�B"r�����{�)E�QJ���7��1XVDA�zo��w==������k<��g���0,"+~���0F!T���9���+
"34��M���O�Cv���aZB�����l
\j
�����0�B��S��\���S�83�����NoBB��I_Y��2 �W�������=k�uw�;����N�`��yN{�\|�s��/�@�����to��,#
������#
*��d��L�����-{���b"����
����JEFh��>���>���"�*��%=������w�����/�O}� 
���C�D�"�D�tH �q��3N
	������T���>�th:$3���^�2�c�<=�L*��N�]J���02fuQ+n��u=wq��5��
�T7�3q�U(������n�E�D_���9���((�")��j���;/}\,DDV���������/�3��� �PTxr��+�{�vw���*"�z��{����R�(������P�W{���>�Q�mN*Y�V	�����������}7�UI�7�������l`���h�0��{�������E��@��:�p���"������84�0� �����E����������FaQ[�1����_Ebvy������n�J��*#����6�}��"��
'��7��8j�00��)�����m��m��>���+���t�U��72P��PuiD���QaPPTE�U��n�d
��w�����td����6P��0���(�
W{���9��q�����:
������:(UP�QX�VW�*��=���9���o�-��\�
�sN,(��*��P�u�-s���{���s�{���}Gn{Q�XX�P`Exu�I�T����ff���h��XA�DXU�^��3^�^{;<�{��������wg���*��-
�������F�T��m%1.�����L#
��
�����<6��X�:��*=�}�5������QX\��}yM�>+������J�%|�����(�����Wf��|^+�r���}~��qid����G��|
Sg�w	���N���=��=���	���'�5[B�*��Yg������(����{>�L�&�1$ �����z�|�m��b�����9���_�F!�Uc�]4�ot�`G��y�$�d��!T;�
HQQe7�Q���1�6����xZ0�(� �,!�i�o>���&��}�}���"
"���.�1��T/YQ��,]����C�����v]��L�����=����������;��V`PDV^�����bs����'�o{F��
,1
�
*+�\�o��1n��ak���8e5�]S����XTDaQE�Nd���rZ�����f�yu����7�XaXr9�'>�Oc��4��61x.�0��0��*��]�3��s����6�����h�Pd���@�B�TQ�M�m��M������|Us������$"*���7��{�st��=2n������H`��xH(B�,�"�8X��� ^�o_n���.��=1��T�O4:����@�`��R�rU0�`���#n�4�o36�N��{I�@P���(}`�k��qDDQC��u��XF�NMssU�V���(��Q!Qc����LZ�*����g-���kO�u�#��	<��{omtXaTA��U{�[�s��AXUE��p��y�W=�vgN�.|W3)�����"��
(���PL�VI%�%��:�(�����'��.^���#� �����B����v�+�*��f�����
�B�
������J��|�9�w;�O/d{��N��AaE�bg}��_�N�Sa����{���7�����E�XFPX��w�g����1�L��eW
�)u*aEUTVaR���O<���u����{�<(*�
�,0�*�zr�8���e�n��&�}X>�
��**���5�rbg.$��r�S�Vc������*�B(���>n��f����]�����Y����g�-�]�sp�K�b�	�J�<����l���A���5��oC�����5���wun��p��=x8���|1��,�,*����S{q����
*B(0��~��{�P"���S�����WV���7j�~��xI��+"��WR1���l}���������"�f�{��p80�*��*IW�U���Hv�DDA�DE(f��?l�q����
��{���������A�Ea��������zVy�������o��4��a@Q�T�fJ_��wmv�:q2k<O.{O���XTUUQ�]���W�b�9<y����w�%�, �0 �1�����Hf�"�,�j�R��kN��)����a�A�P���{�+��������o���E��� 0��	������:
����������>�}�
R`QXAUEC�*��������g;���r�{�:9��}�lEQaA��K';X�*�f���Y�h������3c����#
�������S3a��k�1X[��y��n���,tuV�����7��E�+�5�z�-��_T
Br�P�a����X�z"A����a
�hX�����3{-�U�*��Q��`�c�B�"������5�����"o��|��aQ�����.��_�����@��nw=�����T�1^}yn��#	
!en]}�4iH����zO�I���"��"����������I��
�0(� �+
�oy�����O{�����h���������"��(����*0���<����vx���i�%j���@c/�AMg[�B���T*�PX�UHXC��g;�y�O��^��z�3jv ���t�� .�P�T"��
�����<��9�x�]�OyY���Y5\�:���<K�*"�"�����)�[��������wr�{������"�0��M�s����-)}Z]�w��H����>QT�K���u���Q���f�o�'^���U[E����0#0%
�m��u���Sfqu��-_h�fd�,�oJaEXQaDj1	�j�xy=�n�In��=p*<���b"*��("" �#�w-��8��F�L��m,�[����2�$���:|��[�����)�7%��v@��#���1���&�� �3K�j+�oW�NN
ns�z"w<7��&�D'mI����]�R��vk�����3��Q�QG�g�_���ME�aaE��j��^PP��������_o@a�EC���=ua�Aa��+g�]^�n�aU&{S%o-~�������71*��"�"�
"�����x���wl��k��};s�����uV��rG�pK��^V�����}\1����
(���(���6sy4��8��z������78n�"+B
�
C
"�"�WU�=�M�o;��s����^��r�W�ek�+
�@P�P�M��`�Ma��1Z�[�������[��V:�����[�Gaw���.�1r�s6�UDQ�QTa\�s��
����$���������H�
�
���)��w{�f�����N�W��g�5U��UQ`UE`E��E]���^V���Nv��c�u����0��������zy��m���s�4GA�-��8$�/%o�D�{x�����M\���*N{����eE�����1�;���G�Af���������w�SN/���m����dJ�������(�*���������1

���=���!�	
���u���~��yHEHb�������;��ADaY��6����u�`aFW��w�e����!DQV���w�w]E�Z��y��|�`���aQ`g%�k����<�2��e�
��i��S�%�����H#	
�, ��O����������z���(���k��E��EaQTQ����{�'&y��Y������9�%����EaFDP
�����-}���F�����)�.�*�"�
�*0������M=������{;���������0���@
(G�KV���s+�W�G�"�x�\��"�*��������B
����7e.����J9�N�'b*����(���������ih�jW6�R	X[�au���QTa!TEK���;us'*�;������(aaUE�E`V��Y�5h����TB���5��p��?_���������W&\��1����ufR�f�D��W�MtLZ������8m>�(I���q%�U[�����Y���=��eO~*XUOr�/��kW]�
�QA��/����;��`E�Ty����}G!�To=�r��==�w�0��"*�2���f����
��������7��}�5��aUE}���{�Ga�����T�'��&q��Z0*����
*��=�����9����=�ztW.���*����
�}8��e�wD�hN2�:���.�>�����(�� ���(�9,���!2���a��U�H�\��� ���"�� ���M�o����������}�L��TUDF@�W���(�������w[Rd���������X��URDYNK=��4���Dj<�tM��PR:�3��B�a�XXU{s9|�o���gm�����=<�)�g������R�������"�*}�y��5��>z�}�5�)�*rDQaabDU`�{Ex�=���d�q��r�N���F�DXa�/�	��g+ss���;eS���F;��e�^<h1�]���w����'
O]�Q��t��������wr�����b:$�i��=�m������^�bk���mWG�u�&`N`�U/���;�����\��W�IB��*�,0("����Mw����k���(��2��>~�=�QTTDaTTDA��o�����DQUFU�q��W�uEXDW=���z�~��"0������������e_~��RaV!PUG+����}�i��UXEDDaL�J3�F}�7���;�$EP�h����.�w�zT2�)�g������*�k���vZ��#����u73�
V�vs��o�/:�t����ln�����2[�9Sh��U�QL�>�Y�r�=x��������3%�K�#yX���2v����gyr����]�������{���\�U�HZ-���n�9�K�E���7�u�`�3>�s�p�-A��������������wm���������oR�)!E�mn;�hU������L�Qu�E������	{���bI��4��U �:�s��-�i�y���U����h�}��B];:����/�l�>�6
��YR;�H;i$��v�����<�wTF���}�U�bM���"jT�N���[�l�u���
!E��������M�X6��I:��*�0;��q��.���+K�$��vc��U�5�8�X����r�X�c�Vakl�����+��{�"�{
���q>�����T��|��u�fs�ba�c|lRJ�-��@���K���x��/1vJv��jutUr���1���Q�f�>����~��z�pB�gI�j��*Vk!��Z4����frr8�_d.�Z���y����O;I#l���x�����������(
�V����lT7Rv����uD�r�J	f�j$#�.�A��w����R��F3V�Ed.�o��0�
�G��YI������s���w�M��	�d��������nS���+z����\cn�hq�'���S�S!RZ����������M���1QQ��Y1 ��Px���'%�N��h���e��79�H����(]=����X�B�G���v��(��B:�{��3��h�
x��O�	��ww���G���*V-C�\�'kZ;���.k��p�W�;�'V;?M}��,���c�2��2�*�,7L�,�[�[���
��l�z��;���]��c�D[u�kV��[*I��^�[$����c�uRQ�*
�z�KiuX�W>p��c�:��`��t��8�]OY�}4��e����[�9%�M�8g4��4.����X[Vo����c�r����V������[����u!A#I���os���;s9��r��h�e�a��$���3;������%�I�hka5���e���E�s2��g��Bk��������v=G�+HD�:��`.�����dZ��l��+�4�V�W�F�s������i�JV9�n�B������V��TS�j�F�%>[�L��U��6|�v�u6�E���<�y�-��oH0�b�fY�H�h����J��b������5�+Q�w��hV��fYP����c���.��F�ggr5�)��uk^7�fb5R��iC��3�K�fv��
�������]������[q��w��z�C����������BNB�5��.bV�9��wdtfr]R��yh����@�x*p�{8wk�A���HIOaY���/J���z��x��:�H&2*���&{&|UF�{��kY�uoZ�>�`���^/�l��������v�B���]�ev��d�1]�`�Y�u�|�R��h��A��T��U���L��u�6��`��W.���(k��Xv^��C��w�CoH�4U��i�����m��V��_{9��'f��[���5����h��P��i�P*����_MB����u�	��X/���s/�A�l��c��!�����W<v�V��BT[S�6����[V��d����lN���c��s�k��eMH���������gv������;��p=������NZa_������	��8G 9���8p��Z%�:�84R�n�#����c���.�;���H
wl]�((���>��|�T�0U��d-��\�����z[��)j�d������C��o�y���P�P�w��/,��(*�'�d6r^�J���h���f,#��K�����+3�&��i��J�``Y>Q�����
�v��
���tv|���t=�c�V�vo����m������b]N�Iu�\0�^Fx���2�}���\	!%J|�R����w/�a�cPP����f�.�z�j��7r�}��n��)&�A}y#��������zM����]"�h*H�/6��X��w����q���l�X�N�@��l��Q�u����ka��jqYF
�����G;�f����\u��b��$��vr���rQ�Y�����w�J��d��s�G��e�r]�]U���m=0r�et��p[`������jb�R����6�b��@�Ob��F��.�^\�wG�C�34����J�4�����J���w�T�z��UU|�NH��^LR������pa:��`��(�{����0���%�/z"zr��I1^N�O����3+��[4��a.�{o��m���]t������:�������D)�F���.�_n�N��|{�n����X��WM���n�����m	Z�2
1��?R�������O����J��+�K�%���\0�\
�����������Z�4��\�w�����[�q�g.b�&�����U���ke����J�5hF����3��V\��
�������j�B��b���_���&����v�3j���s6�Z��� �I$�P���wm�^���~,��C��������H��a[
����=A����rd�TN@nJm��U�n��x�����g?=��_���Y�� ���P�7��	�.B�Q�U]�w�(�f�c_�l�4���#����|�#��3Ud�T.5mj�_������Ti�h��;IE}[�J�"k��a7����o�_[f���	I'�4���uPn���X-���-��y7}?+������?v��r�W��Tk?mYR:�5*������row%�>�UX��7j��UC�UkV�e�T�����������������Lsv�dP�Mrx��g�����)u��y�}��*H\�|(� �)$�CqV��j�6�������K��A����5��V]��3Q����F#i��w5�K+:��|7�;�����IXn�a�����f5Cr�j�
' ���U���1{��=3�
a���k�vu����9�Vp�/��k�=���nmR]U���9��8�P6��f�\�ev�>9���~�����c�o���G��yg57��v��%o���|�A[�6$Iu������x
����-U����U'3j��*���M|s	?w��~������bsA��,�^�(��eb�YB���{Y�7p�k��^���n�G*��T8���QZ�r�wj�}�?y���z��w�����4z�T��sG�-�o}�)}�W�����7����VR�wtf�/!cu���xjS�/���������^b��v� f���n4��Tj����f����w������/^����������K����&{�}j��^i	��yt�u��>��=��������?DDD��&��
?DDE�7m�����G8P�l\���B.��C~�D7sow�V�Y*�{��a��Mq�����E\e����������G�Rp5�JoT?Dz�Nv�4�����L�h=bm�������s����2M����C��\�a�<��q]FKG���'kC��o
��x�v���6s�M������n�����4��������~C.��a��]��1o&$�+$��w��,f�J-u���0X��[J�q�#�j���dx�`[���J��X$��]��n�U
���|
T����"��f<M�����������{��\�9,Y9k�o[\f�������v�\��6�M��>9��\������7j�uY2��kVk�`f�CqT���������}����}SG�k�h����;�~x����M|.e���?|�\U�U`�VeU��V]�GUGmP.*���~{�?~2�r�+����F�u=��'���,�Di/����Yj��koB��t�uRm�C[�VsUam�8����$��d��iZ�����P��l���r�k���J8]��N�P�r}B��<�����7A�T.����N*�������U�����y��>����=��K��7d�uL*.����>����8P�[�s�O+�����I'��#wj
��b���n*�wU#���N1D������&r>g��jd�������^!R�%:C���^%Q[Ds���}�w���UE��6�NUP��
Z��U���=1�~yw?g��h����5�m��Y�Qc��e�C�j�nVOO�NO�����	Ud����j�UC��sU@���e�M���J��2��K����@�}�K�2��i^��6�[���O����Cv�7uPwU�j���UX	$��}�~\��vu����E�&���rfq����y����|������Z33S��<��Vb���cUMj�kVf5I�U���g�_�������y�e���'i�*W�?���L��@���twT����^�=�QR@NH�����S5�B��]�����~{~�����3!FI&F�sw�|;�������^�!������9j@!�7��"-mMV0Dt��t#�j:��w�������q�w��p�$����[*��[]d��{D�n=]��"�?���������S6���$3M�NL�5�5o���P��B��z#�w�j��s1�R��T�<�����J<B��uW�|'E3LY����"G����\�����q�A������u���������t�-�X�z#�G+�C�������7�BY{�^}C��^��.��ET�B�����f��j�<�o&�+�����������)r�����n)$��������=���c���,�3{���"B�mh����Vr���^�LR����d��O�m�1�j^e����m��m��
*v�������Y��
�C"R����ge.�nt*^;��+m*�2��E��7�����lQ	������1w��3a3��	%�z����>�4e�;i��Tiz���-+����zV�S
D/�4!����{zw�^����9UL���XsdmT�s��VM��;�o���v��!����}a5�%�l�3�V����byVu�$=���a��n�����`Z�n�Kj��j��VUcu�d���~s�Ww�
���'l~�j�
U�{
@�����w��F���T%L�<�}���3v��Y[V�R��'-�J����f�a��:�������"��^ut|�����S=)HD>��+�y��U�G��l���-�(�'�EK�U���T��V��j����Q��i������yz���WhX���^�t.8,�����sz���~�����&;V:�m�Nn��Z��j��1U���9�~f�����~��6,j��
���v�.��31�d�������y�������"�mU76�V�
�T7j�Z�3I�j�o������{�h��,W����D6|��7?L-��+�)A]3�w�4�n�����fi5�Z�����TcT+j���n�G$�W<�x����1������R���weN%�������r[��A�b5b��=B��&*��Vr��-�r���I>�*��d���}1�Rq��q�����3���G�L��V����?���<U�����x&hnw����S���jF�ev�U�5Vc�C6���}���Ct����?m��S���������v�-��D2�WV����^������VCqVZ�uRsU@�j��Vv�sf��;��?ee��^���Ltk��;�����b�'��b��u���3p�G���Suv�fQ�F��8��m.6�$�8������.�W\kZ���g<���4���:�d�k��x�o9�=���mvS{����/���*'����gz��v��W�}Q��qy�Q66��=�}_H�L*�S�>�����RC��.8�������z�7L%��'E��^�zv=��*�3��>�������������7-���l���N(���N$*�Ov]l��{����������~�O�;�m����S��������c-
�3��4B7G8����=��_J�����'���yJ����D��?��q9���J��([�'��dF�;j%b�v���]�W��j��wS��s�'�`���Y���<��Fq�J7Z����R�������
���:�
;���N�����X��Mj����J/z�����FL�Pf�r@�
hg��qv�3O'��������r���c��K�}�FvV�g�����>8�Y���uX*���6�')�|A�|j�>�i;�#����~��<A�����"����l���G�$��
��s��:��D�j����UWN-XfUqU'��g4����/�=���_���+�.ofk�HaS�+�������Z���E;��RHe9�F����75Y�j���3j�}�B^�%���k|�����5`��R��3�g�$��V�����~����M|t��Z�e�C6��v���@�j�&�>���d^����!B��D�(_��=��JS1`9�mR�p����o/������~�9���a��9���V��NO�i���IOV�?����UI�~u�yU����Kg��Ey�z��>M����uj��M��{�V��~j���v�[T3�9����~����_�~,��~��s�-�'+�N�9Q�@r��KkAF~Hw���[Vn�`sf��75S�VM���U5�I�����g����v�����
����kk��	�kn��!�x5Nf��~�_���q�d��6��j��I��������)Y�G�fg�9�+7��+{yJ�_chH}G��������tU�S�x����b9���I�>Q9Z���`��KmSqW�V�}+mi�d����_E���R���Z��������z�p�t(^;u����v�m�7*���V�+Vn5J-�!NO���v�C���u��k9�]r4p1U�<�
�gJ������~B2��k�������5�+x]g��k�x3���
�������N���<2���Q3��.���B`p�E��i�������R�[����fdotQ����D��j���s���wfx-sam�a�L��{���G���������������P<�.;2�����B��H�
��2����(��}3��?���|��s���V.utqn#��^V����������2�����:>�y�#���C���M��-�I���P�KV����3X�k���
������]c0*�	<$��+n��\���*�X4�l;2<�J���j_N5�����j/3AgmJ��'�o;wV��ip��Ko�k�U�L���V�
�c����Ug=��kug���X�;�K#w�Wf�[s@�aK]���%l��{~]�D��9[z�����K7"�,����	����+�C�\���W��M9��L^��E����,�2�c�(K�����m���qj��V�wuY�j��H�$�6����R9�����T���s��"r&/{jy�_Vq�[�U����y�6�Mv��#���5fkT5j�UE����~������������w�}�������z����,d��FdTg]
_r��5�����7$�C�C5�Z��j��P��9V@(�'�;�y��Z��?|zIlcs��v�u�F���������Z��Z8��,PY���I��������r��;T��Pr��X���>S�~��g����
�oh���Uz�q����
� �=���\Vb�R���o��j���75Y�U&��
��G'��'�� �����_�3*�e]�\��4����k���������^��6 �wcs~�G��r��}Q9��Z�f5Ir���R��=��u����#����G�fPj��8�Lg�P��G��0����.������~����W��.Z�nUa��ff�Mv�'x-�\�V�+���^b�q/s�����VT���{H����'��3��������qT���v�]T�j����6�n��y��~��s�^��=&�?N��}y�����89�u�����6��i�qa�9~�c�|*G!�j������j���KZ�6��qt��;�����Nx)�I����y�;/SI���9E����=�+������3-Rk�d��cT7mR�X�O��O��~�~�/�����V�P0S	�~�joTe5t���e��"�����D�3�&
�[&�{��{�6B�~�����!�����D#F�S��c��B��}�f;�'�nX�1RA�������e�F�����W�/��
����:*}xa��?��Z�7���nl<(�����B���N�3���^�G��L�����m��3���"n��oz#���Q�u\1���������$���������-k�6��Z;���}K6�������(,����R�:[�
���17�V�z3f�8��M!�����_���
c�OQ��*?ni�4�WUy����`����.�Y\Xw����z��5�a�r�2&���/��N���G����:��|�r{ZP��h}�
�u��+>6������ ��U����s���e���n�7��m�]�N?��wY�[���c���,���yw1N��7Y~�$��d#LzV��c"���8���m@��y4��8�������	z���h�z��%�AZ;g}7�}�����9�VZ���GZ���mY9��j��T��~=����������i�,]{)�`���1��*������J���5���<��#�\���mP7j�r��]�v�U�p�U]�y��a{��i9���cjo>�{��g�s�]r�
�?Y�Lo IsV�s�n��n�1j��Y�U��������3-Y1�C�����������<�����2�)������l�����X`�3���HX?KZy}���v�eVf5Mv���C3U��I�4I�*�\�K�k+kq*9t���#Y}������k��T^�(�0���S�~�_��vr�a��uY.�r�I]T�j�����U�[����y�}���o7%d���8��O�k�N�u�Cj�m��n���rN�P����*���]��CY7a��C�VzEY1tI�����~k�
L�����s���n�R^�E�3y�Z;g����z�����+U�mYv���
��Ud����R;j���|=����/J����B�W=�q�6��D��N��"�zk�h]>��yj��	� �'�������J�a�V�T�u���G�1��O�y����W@�9[���rv��_��f��f
��X��$�zn��wj����T���f�&��^i?���<?�0��������u��vV���E��s!����jd}�A��_~��T9��sjRf����%�e��I��?R�� �*����"~�1,����}fyO7��Dy����1�����\/C��wQ������N:��&Oy�P�\���p����;��
Lr���|�nc�i�\3��%�>��CH��{u��s�����L�l*���PL9�����N���>�^��%n�}nm�3z�=��Q���O���7��><���[����-�����������PKm�]/@��g�z��0���<~�z/�1Gv�zA���G�������%X�����9�{���J)����8�4��jI������O��{|�5�:�5�����f=9��l��D�c6�8��/�������m�4<�u��{�O='�QYH��Xp�U^�����Z�q���5���x7�k.��x�nZf����fgn�i�m��Vl������-������L���w|lb��t�vl78�����h9�-��$����x��=��!�V�9��/i�Y`�k-�_��	������M�s��g\z�������I2�f�Xn�`�T5�a�j���r��kT3�dxl<?z����[af_�!�]c	���u�H�c�2��<��k^l���H[T6���j��I�U&*���"n@����t���������qOg���is�[|��S�^��6)��^�����7/�{.�Xr�����b�2����f��b��I�\���������fD
N�lp�4�n������1����y]����=�p
I%6���c��M�7uY8�����U�T?}�~�{��7���_���u>W��kG�?`C/Y�n��n.�81y�79~UD�O����kVLZ�����mYr�q�����sT����Z�\�������*00�o�8sK�d�!+v���=��������an����C5����V�P�U�V]j�@/]}w�/�/���,��i���ce��q��A)����QP������4\������V�.�L�]��1���n4���c�~s����9�yd}�qV5��s�,&3���w[����v�:����{�_����*���qj����7-R]U�-�f������y�����~g�i7���89�w�eV�/��R�`x�����������P[V����'+T5�a[W����@?p���=s�#��cUn�t�>��U���^���9�7v��j���j���B�VL�P.U`cU<�>�I>���c��}�tI�0�F]��|��}�ie����]S�R��U��������\M����qD��l�c��N����FF?D�P^9��l{�xK��T
��0�n���de]k�c#5b��v	�������+��[�i�����ww-Y�B%���1`n���b{����s$T��gE���}�A�K�YM<0�1���#�4m����
rr�z"#c�6�G8Q�s��b*�F��!`�.=�Fp��������+x}���uD��������F[����Uf���u�������6�f�'e] k%w1y���P����3n���Nv7=��f�����%�Q�4i;��T��{70�m��'�m�_�[r��#�����b�[C��z���u���t�����*d1>�}�d�2������ ��
u�9�0R�B�����	T�q����	����s��5��X��;�v�]V4nX�i�����t}�#��]��VV��k��v�~>w���I��G0\�V]J���2�&���(^d�}?{�
��9v�r����Lj������$�}
��'!�|���L���x��D�8������>�v�~P��g*k���3���I9)� ��������`���T5��������s�=}���|�e��Ve���kkd�m	���m��n�7k�F�n|�?A6I��Lz���Z�[U'6�C��uY��d~�]����KP��*�V�Vz�!�f�?���a,�e���|F�]y\� ���*�j�T���r�j�\������1y�~>��{������:�8�53B\2����.��?J��0�"��ul��o
�~���� *I�)����T�T�X��'3U��o�.�^d����W#�����WU�����
���bT3�*Y]�+��
9���~�fU��Y�j������q����%��]��s�s���<����n������e�`}{�yf�%���[Jr�HL������+#g��|)�'�F��UXZ��*�U�����x���5/M�������d��+���YS��c�
�KZt�J���h����z��tuT�U�����-T�U���Rr|m�������w�������v��{���a�����Jy���B�������u�I����T��a��C
b�$���'���'��u�����6>�?<��zn�iyz���*$,F�E����k�w�~����X��AU`f5@��'2���wj�U:����U��
��7�~�3��8�����RO�K����\�(�����|�z���\^Cx����S��U�e�Z>�Mul���(�O�s�n��Qn��3[��S�����8��4����{��]�
C^g��������u����h�6�������jTw;5]1�]�xs���/�����M�-��_����-Wy5JF-���W�����H���5WR�����^[>&�����"2���U�|5�X�y���N�:X�D���
���M|e��Z�����c`��Y�vlB�#�������2��*$��%����wm������T���|+R��Q{Y�~���N^swX	�����%W�k���8�(V
F�b/7�"��J�R*t� ���v����������,�@B����c�Pe,�yE.��8�]]\�P��f��9E��<��N��zVMgp*1b3�Rk�RO�G��9���X	e�W4����f���W���M�>i�>
)%$��I�����U�T��z���~����G�tH���lR\a"�����v7�2��M����Z���/��nJi�&cT���-��Z�1��R[��D��W�@j>�*�}��P��p3���b����,:����������X����&�T�j���
�V��.�>�9>F9i�l������|��y�z�,3�&u��K��biR���W���zq���w��n�r*7'�TNA
�|	��m��W�T�Z��q�����['���?�J]Dp3�����w�d`����9x#�����c���%�rHuYv�6����3v����U{����M���
���
�P�.6���������Z���f�y�����;�	� ����+T7C��3Z�n��]�	�@.��7���/Y9�g���:k2��
nT����@�'(�Rw����L��}r�{�<����?���mP2��wU�����T��T9U`�s=��?v�w�w*w����v7q+T}���S[Mm�r`'�6��O�w
�0�p��o6cT�F��\��cU&:��~�:O��������!����@���p��K��j�������=�������1G(t��=�NH>�9>[�U�����c�VkT��������U��;]oWmk����j6��w�k��R��*�:4N�&�o}�^�~����M��-�e���j��`�������J��L?�#,]�T�M^N�Fl�N�3���Y�����c���b�}^G���U��^	�
|���������t�vg�������K�W���@/J�����}�Wv����@��fy�t��������{*�[AQ�{A����Co���z e^k��NgFO/G��b��������s��G���j���*�����C��_]D���#C^��9����5��d/{�����M�I������>��_h����z#�T�+�\�~�������i��(i`��x��EM/�������������i���'b���7!}U��gcp�����*���S�g�������K����Y|j�S�(�h��Q���k����\���j�������N�V��r����

V&(�]�'`�}�J�E��#+i��u1���e���*e,�����E��V5���\�\�*�}y��D�b��zZ�l�-)��s-���-�����Mc��'�W8����H���Z���U��eI�K�f�rW\�{[�}UR�T3Z�n���U8�X�I�VO�tg���`���f�Y�=�����{���q���4wY��#b��:�z�������U�m�5�L����mYkVr��]��<���/�����W�P��9J��_g��g�^�C5.~�S	���oE~X���8���1�5��Z��Y8�����T�?
�F�b�y�n6���+�	��O^��tSu��F=f��#eWt�;���|��7C2�v��j�3U��S��|������i,Z=t?=5u�Ycu��q�?o���u��A��1���3�~��\j��L��9���j��T7*�j���_����}���{�~?T�����`����>����b�v����*'��~*�-��X���U&f�qVj�]�O/�S�Q�}��V��:'�]���p��K%$2��b$A���684,�]��}�1�a����T����NmV��E]}&��;���J�Kg����2����Qk��W)��S����@��[���.M���m������C6��UC��������U'j^�~������L��fz�53�����\���[�{���x����OE��j/�3>��V����C�T.UH*��j���� �F�_k+��a!pg�S��y����8�s`�����{E�&p�hp��������.Z�m�I�j�;T76�5j��V\�r���~�7�d�8��~�V�;kG_���u�����3�U��q���4���������T�E��b���8�����:=��UQDUD^|Wq�~7Y�������Y����d�
��
����;���TDTQa�}�yN�vfIUXb����UK)��DO�c���5(�*�
+\���k_w���#�{���T([������������Z��v���R�����F�
(�F�!�/�d�kP��	��k�yh������.J����0jz�����^O]�m����TDFc�w����G�M��UEXa7�r����XE���}���DQEb��^�^�QQ�{�Tk�E��U+�s����R�QDs~{���*�q��{�Ee�(��HPE�Q{�r���S�
����w����w�X��`j��K�V8��(��
Y����A���#<�e�������M���~��'Xye~~��A1���4�k+Z�����x�}|�����d��-DaV�o��b����y������H*�#�������{������ �SS5S"**�#���^��ppE�E������EE��C�������
(������F2�������$��mv��8����b��l��,i�"B���k���]zK�:��N�jD��&��q��K*�}q����n.]�M��
_:���L�2m����*����f�{]�'V_�����a����K��((�,����s8*����l��\�����FTEa}Q�������(,e}[������Q�U�b������e)

����2"��=5]�M��r��~Ss�gB��;)ns{/�J���1�������0i"n�����y�{bb��������c��vS����3c!�
��9zNe����������he�����KXPUa7���;w�g���� �
����Y�r����"�'3����.Qaj�w������� �0�M�4������"������_o������b��]���O;���#�0���y1�5m���-U+v�2;�L}�i�1������2+n\e��lX{gE�@�����������~0�S�::��O��}��[����l<��n����+4C�V�O:QP~C_^Q3>QQE��c�����l ��0�>��o���:+�����zr}9���0d����g!QTV^�j�������Va�+�|x����LX�
(��^��w����EXE�`A#gk���7����}�U��VUS�$����+4���5v��.=B2r����O���J�����N����6�(������{g��G��#;ds�����:sC+����o��$����[�����{�� ������{�����$ ��w����&�������'2(XE����mt�UU�I/�z����:�"�������s��N��UB����#Q!F���>{hO!�J���7���@���T�;�{�����r����$El+5�'UM���?�V���y�M���m�q���<L��A1
z[�P>1��w:�`^��_����<�����(}\XaQ�=��m(%��n�qT�ETV�?[7_T������|�3�|��XADH�����DFzy���P���(����I[���
���2�<{W���X$�9�����v.������vST���i�I7	��&w���^zj�uKx�7�����G'A9��b�1RBdB�U2����]���:5��:(�_��n]��O���9���Q�QQ�6������y�TTU!�{�Y���������DE���(�;y��,�THD�O�l�XT����������"
(��e�~�#E!_g{�W9�Ux\���Dy7	��m'D�
���Vs������#�������6���*�(������:�&BAx;�vK��Ej+m^W�E=N���do���#��{�����E�\-�zWxa����*T������I��PPUQ���7�s}��;"��0������A!y<�������D!EF��v�U���	���w8�*"�"��z��}gaVL����3(���iU�x?oH��wlV@��[��������5�P�Df����de��	��z��!.��%�LZ�/�d_�^��2�:�OH�jM�S�m�2V{���Y������^��"*���k{��![������$,)������2|Oy�FH�}]���x����*�����I���Q�TO{�ovy
"(�+
����g��60�,
 1��&����}��U����j�U��L��a%}�6l.s�]YY����4�X��*t58�g�n*�=�W����cg�Kh"p��]F`H|�-��6����������K�K����]���������J�~�EA�]�������*�\���w#s:T"���?u�{���
�,$<�����x�����(��1��d��VXD�9���TFE�V���}���>{����@P��,l-��:n�CL$v�7��8y��zN("���z�>�yJ�6�o��k��Q��fq���bn�F�[������>�rQ�����vhh�| �������s5aVTG.��w�����HU�3�Y����Y��Ea
��}?]�w����B;���c{���PPE�EFwo������`DU���������*�,$�D5�3!�������-]N*�����]I����-����0�*��P�:i��^���R�/bkvl�<�+�GPUaX�7k(�������MVd����v���U��
��(�*����L����+�U���~�����z<*���,"�'sY��0oY$��f�1}��[�m��M���"0�����<���w����4�;��N&�����\�����%_
��'<�&�L�Y4[�W���x�����RDX�Q�,�{L�L����Ovk���AQU�a�a�;�����;�MU_9�{�����r�2�UTDEEE�Nu�2zM=�f2X�^��z��u���+��B�-���O ���6�6��+�RQoWT�5����S�]9�L�:_����;������*��,v������>������6��+(�(
DV{�q�S���HPF�������1�����*�|���%��C���o����uQaA�]��q0��
�~�*o,�X���g��Pa�`Q����5R�����UEDUEPN;�0C�*����UuJ���F�M����
�*�,,e���qg�����}�g�����f�i��+� ���
��${.����_Y����:n�`Fa���Oy|��N��W�5��vj��=.�{^���A�EE��y�s��g���]�\=���v���������(�����-����H�/:;\A��gs;:��,(�+���g�����=Wmc�f��KL
Fa��a�o�Mp���;|�{�s�k9������0�@�L�kS������	�nf=�p�PA�XEg��}���<�o%C���xeW=&
����s^���svt�>�[P)M$����c�y���Nar*a�oS���7�k%��j���G�uvk������^��9��R���{�'���*"������=�9�UTUEPe����UT�b�k����1
�������G%QRN�]���#

���{�g;�����0���O�����=��bEnn���Y{C(P�"$0�"���gM��{�P�o~���.����;���z1X`aFaQaQQ�S�}�n�������+���������v����� ������
��Zh:�gJx��$��C�WO8��`0���(�svd����ol�=��f\����zwx��$ ��r��+jyM��gvl��_��{�O�{��+"������.�gZ�t�%n��v�=Z�WO(|`aU���������};��>�q��)3S}��,"(�(���Ty�x9��Sk&k��K���o��n���������,, ��.]\X���$z&sS�#7f ��������� ���Ze��g9>�TV�v*���O.k	��<�=��#v(
�~�������*�'��#���i������?f_��7�X��8�Q������`��T�H�PM��^�z���X�
"C���*1X��]''��x#�q)��]a��ADQ��==��������27����:R+,,
��n���~�L"*����"��+���"����+�?3<s������������0������������d�uj�0�8fTJ��SU�F!!a�aB��{_sy]��N�v��=�._o�'z����0��
�������Y�{/�f���=��|����N��oL��9����C�T����K{�����Z����[�X���UaHUQ�$1u��ecr���U���9�1� �*�"�*��rf�}�k����S��3,�e]s95����q�EUU�C�:���i����������+��$"�
��"���;'o�'K�����r��f��EU�Z����mM��k��Mr��^n�����!UQQPE@�B���}���2���r�Q�y�@��|���GcV��2���(���������;t?;2�e���=QZ�����}[D�%%.����4���L�:��Y���2���tk����J(��#Z�s�r{����
��*���o��_;��/.~���?7�TTQUa������EaXTQc��r3�>��XRa>����u���A�,1�y�M���+d�L�nv����XV�������7�{qbFiNk6[k/S�s�B�Uaaa�9������Y��r}=�}]8����"EXE�UA9�1fUr5o���Vk����h����{���"
*B�*6.�������I��k;y�w�0�$pXX��DadM���dL�OYHE�P�|����onc/��n.w;��7������]U��9'����B��*@|(PS���Y��t)�	d��;9�����q�s��"""�(���(%�9�4���4�����,E2�aaEUT`U������Q���P3X���td�7(T�|�{�B��
���,g=[|�l�\�����{��el<x��~mh{}\{E��
�{}y5Y	z����Z��3�����G���u
Yv\��O���4u�������Y��2]�,�;�x�<(���"h����b",(�rw'v��`DVU�3g/��-DDFC^Ul}e����������EDX��v������W�3e����
�m��o �
(��9���o.��0��0���
��{_������7i����F���	��s�*�����*��
_b�Y��W{S��_n��^�{3����(�"���"��{�UQ�G�c���Y�i�w������,(��(""�X�rg-��zi�w���z�)��*�AT�@X�mm�fy%��/��������mw���l��0��(��
��<�eog��}t��io;[%y���*��,"��3(�%�:�s;�������3�s��m���
���
W�KU�o6��*�;��.Kx���XXE�aUUUm��	� ���iZ�gJJ�����U�w�H��=�����}������<�1%�j����=���a�r��1��\b�s
�M����g����?9�{b���.r:���i'�����q�y���vs��%��B�����g7��������u����z�TAR>�����g'9��������^7���U��W]\jO�PaEN�Z������aaUwe��}�q�I"���33"��U����$XUa�FDUc��O�}.��(��7�������[���;w������ �7�V��T����[�<���fm1N��
��B��H�Ow(���&�]�������UDQE;��]5(����<��GM�	�����0����s���+6���py]���^��Z�d����Q`U`aDP����s�_8������a�/������-U�a�b
�g��D�����d�Nz�^�������


"�����W�9UW'}����{���O��<���i��M�qE�Q��J�J��v������ky������+&���C
"������}����p��	����:�T��;�������d��I���@. ����Z�4'���MFX�����!Y��V��j���Q��n��8$���������1
�#�r����vS���?syn����*0g���)�w�@��"����Y0TAaXD�W�������AUPm%?!�C�b�(P��U;�����qU��VUE�DFA.�3�};����w�g%��oL�q��{
*���� �(�7��2+V�wYX�gZ\hd�����"�("�,*����or����{/����wv�fw�H������������1�8vf[�{<�X������"�!;7��{3��a�<�����O�L/���QV�U�PAB�WMU�a5��2�[u|�Po{�R����w+�����5��VOg�e�gy3��tj��0���|�}�S���y<�N�r����I}��eaQEaH�N�k6���|*���Ev.4�^��t(P��DQ9�r���Sg�y�w��de�'b���i���������D��cM�~��.u27�^�Ptk��^	�_M������#{�u���OB8��v�F�����)��f
������!TEED��M�2��FDaUEPDQ�T�EaaQXE��U��]M���DQ�DEaD����{>��DQE�aQ�~����\s����F�����n<EFXa�U����"����
"�(�{�}yH�I����J���2^�!������]�V��|�ge��XO:]VY�;��]��wt\H�w�]k��um1e@�������W��5:v��o��nG���F�F�/h��^��8s�(�����F��x).u�'Ro��_}���F!�K�hY��3�X�O��wV���E:[\uA��t��J'ww7*����-	\J�O�G2-�K2f����0\����P�rQ�R:=��'�b���#7�T��������oT��M�:�A��4!Li=y�X���;�������q�A�v�MY�����.�o}\��ql9��jWM���-��gK�������,sO�k�����j
\�S�5�Y[3���Z23\����b���[���V������[���sEu7�m� �I ��r=��iv��i-�x���a4l�B�p�e��uu��m%g�� t����w���q�3����*�����EY�\�j�r���$XWrqJ��#+���vw2��\������"o�SC���!�����z��;a!+`cFh��5����
��f�MTU����"|����(���9�����������Q��Y�NH�A�H^e���
mmAu�R|�5G����m����TcfO��+b��ix1w
N��hn�i�-=��E���9w]b��{�{r�]�m����gJ`�R#������C�� �f���.��Y(�����R�N�wTmh�ln����O���A�a��G��]�-b��yu���b��Dz��t��l������K��N��lT1��9����0�Y�����!Y�H?J��|U1�c����t��J��v�.����4t<5`���Dmvl��3��Z�}/iK�e�lQ'���O��EUL��5��
����VC���!�A}w�GH�b��
�D�yS;�aN�P�j[�������1Y��f����d��f���R�/��7�6%��a�S"[���u������]�Yz[��3��p����-�������y	���AnjY���><�:��/^���19]]�X��������P��]R�N�����Pd��H1��tK9	�5u�������{@�����Yox��9Nw����p��b��\�U�k���Uwp���Y����,G�""b9��z����,��gsZ�<b�R�����ax����d�j�]���TU�����������
���"���z�5�We�B�]���i�OWn���Ll������8[�>)�6&2����y3c��'O;��4�v�g���s�K����H�$b��j��%����WwPV�QJ1mgg`A�W���p����$�w��:W'-r�������]bk�&�v��[��p���D�Kf�v{�^{��xs�9m�{�wX��u}#���B�����A�[k�r�jH����>V�."�s����q ���b�>�W�k����o^����. mn���S;�sv�	t7��d�J&�T���>�_ 4���� AQ<)����s���
_i�y�i��������U���[��rqE=�r|\'�eRgNmu�s��n���61d���u8�����&�7
+�9��E�J�r�������"�
��hew��oh�tS0u��\Z�SE${��
�0�1�����Fj���l�u�u]Ry��}��5�n�����C*�)���7���"|�Y8�Y-c���
X96#D�����N��� �����q���OW���Q:�]�j�F[�����:��5�70��!��$�S�qZX��R*y7�����L�4*�f�W�(�r�-����8����'j,�>@���+T���l�Nr�����r�{T���t/����(��HWq���v�Qw5��7���@}��]�C��/�N������^����u=W��+��&��.��]E<���al��^[�mB�?<ziQ��Cx{�<�b�R��c�������4�gb�v�o�$k�fI�s���+��gP�1R,;����&s�{O/xcu1��&�����J��(�P�;�m#�IP�i���jfg�z�VZ����ok��vg a�G2��!56�q�����%���j�;Vq�]j7��u�3�W�b���4��,y
�"xe�j���y*�F��A{}����>)&S�70����}<!��gW/F?V�|�[C_|�����0T��[��_Y�m�4}�:��Z;����	��y���[��?
I@��"c���'�\���O��Pq�
IL?�y{[}��������
nr���v��C��k�}��,�` N�'�rXWS���>���;��z����u�����w������}���^����S�>+�o�L�ab��w��[���X^q���F�a���DR[��R��P�����;c���M�b���q_wJZ4�M�����SFPVk �Q�7,;W�8v;�����Y�(�AZ������ �:^��g9����a<�wCv����&�%����jP[C+ �^6U����z#8<�M�=(��z{bV�F�K`<����������<�;��n�,�Y�:��}�;��o!��4��Y��]�n�r��P���o��w	\d��Jy�ySU�G��d�W\K���"����}Vk��?��_�$�%�.�1��1V]j���f��������Q����K*�&����!uuT3�'G��������}:������I��I�U�5�;j�Z���[�O�]��O�\?��"��o�*�Du�H]HaZv������w��D�
��\Wu*�v-�,O�+?o��'�"��
2Fm��sU�UYU�v�F�@�hZ��]����Q���/�_TM����4bz~������j�^�]�pz������.�asU*����uT�n9>� ������������5������=Qy�~��YE�K���N^������(�o�g�' �'�}Q�$��������V]����{��Q�o�C�"9���3����|�F��/���:��~+)�]�!�sr��T�U&��'5T�j��UVI���>�B���k�]���=�����x%���0-����tV�87�dzh=���������1��r|br|���T��XV�
�P�����O���*oL;���V���iL�~��B��@��ty�;2��������Z�9�V;�H��uXm�>��k�u���%��k���7%^��Wd��k��L��'{{w.7h���������~�?=�-�F��Ud�j��Y����T3j���v���f�������`����w�����?\��~w�-����oT��6k�/	��<\.Z���9Z�\�d���]��$�$�9$T����^��W��q�=��`{�+��p��4����2l}�}��S�C�BW��b���q�^���b��uh�({g���x]- k����x
���q�Wq��fh������@w�e,��]���P.��yt}y]R�gOZI�����KJ�4�����|>�d^�}��y������~���c�"�z-q)����������	��V#���k������f}���������{+2�����"a�nOJBbs,�n����}��B��7S�>������c���Q�W5�<k��y��:$��ch�(�,|��=�����A2��t�����Q�ux�I
��WI��d����[���<r����������R�t0���N@��`����3s�x?�kW��qQ�`�t��&.iR�Usb�D�����k�^�����v��}^qA0�	������M���>�3�*���
E�
����<��y��}4�]Q��B�M�����l,&�����6X��o�y�g~���8����������I�VC�j�7U�������^��~����_s2�43[��j0u�
����>�������k�MY���;7$����
�YuT�j�.�f�
�P��>�u=����I���gr?����e������Gq�neUV_l
TRC^|�������6�3-P��a���Q'�l�h�}��UZ��v�?��=��|&�{��R�C���$M���o��g7{{�w����&��j�
�Y�j��X[U���sy�����]�8|�{
*�}���#xj�D?�V
5�(��`^vm��������V
������Y7e�P&9#R@�����W���K����{D���`�?�0�5k�W�(���V��/������@�V\���U�XWU'�mS�T�}{�xu�t�_���p,v����[+���)��%��=�p:��2��5.��X#���P]VK����VmST9�Y[gI����~	~��J�i�[!����}�;�����{lkxl�b������_��6�Q�RU���sv����}II>&9$�s�^=�YxV�~}?����H�����������K)��6f�,�=��z�R9�� ��&;T���2��g*���g<Y�99�����;x#:���C���<���B��m�(��&<2������K��r�XV���f�v��}��$�����x����o_^��������~�{��%�7Gd�_j�:$J�Q�/���������������~�fo���7�����`�����N�)^���wz}!y����p|@^�Fv)����M�
}���)d�����z5�u������B[67�D���R;��=UUT����`�FM���G�]�O���
dk~�����g�A�R�q���=��*�)��/R�����4&!\�m�{��T����i�U}��Ml������_J��� ��}����r��8�F���Fs�E�n��J�{3����og};@��FXY@���N�����H/w�8(�]l����&�+�����36������+�*�zO�V�:�$��Z��J���D���IT����d������beq���Y��9�c�l�
�VOx�+a;����w�|���E�������Ry������i�K����5�?0��;X'1��3Kmw`���4_G��������A��
������3��U%��`��������6�O8���������
8/��1�iR�4_Z��if�gs��0��������
�N]�v��U��g�����7�O����o��y��������^�����A�����zW������������Y���XfZ�m�9��Z�G*�9������u�c0|�P��7��R��{_qCi*1W�����t���Q���?O)a�Y�j�*��j��U��CsU�E�Y1�$�-��O�;���@�;�� {K����m��{�{ed��/3�N�jUm�$�����T{�.=vl�x�I����R�Tr��T6�P�I>!��i{�������w#�^'�z���oo�������LJ������$-���m�������uT�I>&�����������B~�K�������[j�E"�~=��2b�!�����GN���b�*��5e��9�M�Rc���PmV�X~0���w��*��g����u]���-"k"�����bfQ������}���8^�������'!v��M�T�I�U����k>G'��{��S�>
A�?xklxn�;����Y_�~�����z���?����VLmY*�;T��7`19>M91�>k:��{(O�������y������[I�����pP.,����M:��b�C��;�QnO�m�r37U�j�[U�T�U�j���|��U�����gm���"��T����[��8�[��T�]�A�������G=�oE��F��!�o��(I��J�����������n"+8�G!t���zw��cz�8�}�����t��X\��AO>��������c�����S���#�==���.y������,�������������2��\bY��q��x���r�'~��;��K��=�A�a�Jd��x""��g�}��x�5�8��" �I������������z�P+f]����>��Cf�Ah��2��^{c��bd�������O6��E-�[~���8�8�W��:>�d{%h��F$ZN�jJ
�#�������-�G*��� 	:74�2�X���X-sOC�n����k��E����t\�B�u%�+����CE�]�m�.�G�v�Sb���F�sK�JU�}�����	����%�cV�������o($x#�p-��
��"�s\�����i�]��s���^�������o����*h�=@���]����h��g�}r}�|	uI�T��H������U�cI�:MG��y5���	���������Z*������Y�Ap�n�����@�]���uP�����*��5fV�sj���KZ�}G�w;�:�����6�:	�]/��L\������(���I��^w����37��q���Ve�d��9�XsmR[�c��������3������].]z�[+/��������dP�kQU_��F������4T���Y�VM�T���-Vm��U�d��^B������3e_a��t����MOyQ��i>���Z
���������f����{-���I	� �'�j���9�P��#Ua�v��_���oOo�,T�7����!g�����_�Q�x1�R���R������LTU�mY6�dURU������$��8vk�u�_�'j��y��b���I�,�3I
^�c��M�Y�����w�o��������L�V
�
�g-T��b��1�.Ua���y���Sh�)�3��?���E�P��&�{_��[���wX�7$TrO�-�P��f:�9j���@�U7���?�M�O���%Jv�w�'x��T�s	��m�C
�������[���)��TU.�2�A��n���$�G$t���W�s����c�,��d�~��>��&���_�&��D��y����~���j����m�Z��T9�T.j���mR?��=������)����[�K�U
��^�D���u���-���GAz{���VF ``�c�x}8g
!R�y��P��[��k���t���y��0�y�����;��J��<���>��]J��"q��z`�\}���9Z�����O!	0�C�U�f�s%�-���Z��%u}��-�3����s�{��5�+������}�3�*a��W��R���#���T���j�%G��F��r\���9�q���<��Fg�Z�S�,K�DxQ��|p�w#�_ld���������������M�p�����i#����+v�P��X.PaqZ��y���������2��
	S�.p��4b��^G�4�=���:��-�ap��u	�$��f�����N�&s	L��3�5{v_M�Z���K"�;wt+��������l�1Q�U!�?{^�pex�V9�c�s;a��aM)�J�������j���.��q,�������i�(��1/?jv�	^���o���{u��6�"�/oz������6�mY������@�U@�U��:�u��~������s-��7�uu��������'����2���E%��g2S�rH>	��U�uP��B����8�O�Ww^QP������r���b�u>U�V��S-�=�wA�0��]��~�'��cj�kT�T�j�Ug.��d�������eW���1��f�*��k0�J[%������2m��Ybh��`��~��!��������qT��dqW������������W~�������Y���O����0;1�c����G5m�y-2�VH��#b�[��<��$��RAE+����UT��I�j�������}��g��v�Y��������,�v��u<��+�\V�MF��	����&��;j�wUUakT�4Id�A����q8�������b�P^��!#��_�ky��Tw%�)������.��y~����D��S��M���N:�qj�]T9w*���7����]_!����6q	���������~�����0����Vk���v1�w�~��7U�QuP�a��������g5�WU'y���w�o��o_"zQ�G�[hm��P������b��q2��=������'��>���Q�@nC��T7����[T��d?}���U�q�T]o}����_]J��L��f�OZ��X�����S���MVY+j���Iv�1�.����)���V���C���v�u?kK������[�����j��u7o��p���fUKm��9(�Y���
��z(���j�6��\��&]����	���v-��Hw�����EU'����8-�?DL.z��-��S����c��c�;\1�W��IN5���}��[��<9�������
T����	�>�z�D�[���y��+!{������T��y�>B3�wW��5Y�����&���������G����V�������z������>���:m����c�G�N�.u�v]yc������}�������z8f�G&�&)��a{����R^�b	YT��Yy�1�%n!��W]���N�\�Bs�25`�O����
�)m�oT�����>�p�,�L�L�}	���k�j�S��0��Z8����4�R���r*�<��^\��E��>��d��'v�g��ha[�>��InrB�qZ�*���R�(���s58�qV�UC����5o�OY��,*,��0R��7������	�'�j������Ua�V[���W��.��?V,p����s|r�:5��j�N�Q��{EEVN�3N3��~r9>��emY.�����V�C6���g���~�h����\��s��[�}����YU�t
),`�����Qcw����|��U�[T��I��.�I����V��k��m��������?0o�c�n�P*x�q��X:��Tb���e�g����������G ����kV9��f���Im���}��������o�1�n�R���F&�F����l�+AV*�{�E�:��M���Z�j�j���j�.mX��[r���;��H��6���������Bg�����??'v�,��R��<�zu��fj���!�j��U2�������Z�.�9��=��������%�2�����FJ��o��%��HL��h���H��o�.������NO�qRfmSVM�m��n�U';�m��Y"~J��w}]��5n%�Vs���/��B����76�cf$�����
*���d[Vf5@��Ne��}wVL@I�}����r��t;�*]������W��!�[Q�?K�9w�s���?�|&�I�T��M�2��uX.�j�9uP����g�j����F*���N�z-���a�����������
:��~�����T��I����Z�f5c��3S��+���x��:V����c��\�
�o�P�-��sf��8�IJt�P�[�4�}�q��/<h::e��=3�P��>�o-����p6��i�z.���P���v5D���e}U���T��h��c������?}[�T�s�In�p������&�O����M����j���K+*�L�g�|>
4�z{RJv��=g���=�c�o~}�sF���,����Y��}�
r^Z�z=����"����O	~��3�`������K�T�1���	��]/������U���0u��L�.F�T9A���.��:��v��D���mzU��<�u�C���EM&�*h�8�����9G�0iJ;Sgm7X��y;y�y����������k;),�]�n�Vn-�3p��x��sr��|5t��Q�� ��wV��:���a>����wr5�SG\5D��\��X���A�U���EBt�2�Un�$�	�; VZ�]?4��R��5��>�Di��cYX������0^�?����E$��$	9#-��T+U���fb�.5g����{��.n����u=q����r��v��%�9�dT&��F�J��������r|�V�C�T�j�kTZ�kT���6��w`L��~L�c�=�=��1����"��s�,�Nb_�ni7�_:�y��T�-Xc���j���q�I�U�kVN]�����s��>P6$o���s�:oQ�����`M��|d��������dn�p%~��'���v��Y3v�8�RU�e�M������������[B"
�����l	�b�����8�&����6�zA�!{�UB�����k���T��fkr�r|������8o��-
O��������^��\���D�`s� ^��]��gT�����8�wUg.�'6��T�*�f�9�Rs5Q��)�[�>�c��� &~q}8������'���}r���F�i���s3�������"��nj��j�uRnUI����u���s�]9sc���*�EX�>u�6w��)����*�;^�$�KsGM��u*�5U��P�T�����r��|R���{���N����~���nt]��WAy�����n�e4�T���_|�0�vj�g��s|!nO�rO���>iH�5a�����Z��uT��s��?G� U������Y+���T����{X�yv��|�s��W���nHe8�mY��b����f5I����MT��@�z����������#8��j\K�S�gA���4r��EJ�{��&�;�������t/w�����Xu��2��T�.jc��i�����
t�?�*c"x?����C�#�T������a{��8Ft81�4��DVJ��D��iK����)ZL�CU��z��p������ ��z}]��q����s8�Kz=����{�9bj��z"�U�>d�E����t�����hh����}_���Y��Jylo��N�����"�"A�u���]B?�p;o2��v_�{�OESB���$a�)�=�7�)�U���x��@T������w.�[�_p�,�(���s#96�8�H�1s���hWu���:����G^	t�r�dC!����Y��76���A���|���,�����X��4���3_EGU���f��wu��u�{@�8a���7B�4h�kr�0v���F	fz�Z��x����)�y�}J�
�\X�H-��uy���W��=V���~w+�W�k�uJ2����X�o�������������sUM�Y��'3U�
�LDS��K�J>=�v�c����Z�#���Pe���^����n_p�_���wj�b�n�Y9�R5P�j��$�8���Y���oNSk�;A�����3�M�����8&��0�pc���X}�[����������H���T�uWuY����s�������	�����R;��g����f�d�����<%�����?7=����2��v�1��6�+U�Z�sU�I1�7|��K�+b�	�P��	�J��up_�Ua��+��.�P#{��w������aZ�]����T�����e���T��.��0t|F��:��sk����h����O^Sc'n�a���~zw/Q����Q�03@�U��T�U�U7UI���I �y�w��m��c�UZy��Wo��&�_A��~�?=B��Kl��}z�L�Mq�]j�������}T���T-������fi&��TW�����O����Z��33g�������(�����	B��z�<u~���S�j��j�v�&cT�r�r|RO�����w�J7���V������_K���~���������F����;�%�Ol���)��O��Re�f���U`�Pq�6���Ty�������x�~����[g8�q���>9/p%������Dx�5w���������9����8������V�T�]�_�n>������gu�Z�<����N����/��,NK�"�������U�^��R���
tSn��Zz��IkpB=����8���_v����Oc;�<�#k�2��\>��^�P���\�
����}�r��M��wz�c�;Zi�paQ���5��G�l!/@���#�`���Y����7�#������'��{��T9K��r�gR���tmdL���rE���\q���sB�������#�z�E�s�?�G��>�����3������;�w���|�j���"K�Q��T�wrT�BI������)�u����;M^,|��)�jnQJ��j��>�n�k^C�J�n��l�-�]�����;���%=-
�8�9v�}Ze�9�.]�2��P]��X��K��7�;��`����B��5�N�(n�,W�vW�t�g��>R$�+����v&����C� E���{�J�Ea��=2��s�{�E�p-$!�d��	{�Kh��C��N��������t��o�����r��5��5j��j����.�
8I��~
�����*�&����0~86�J��\��o�Fj�����:�mj�;�u�~{�~���n*���;�e�RuP�U
���V{|����������%�.�'lQ.enE>��!��oG�d��a���-��sR��++��� e5�I�j����Uj���DS�&��)��!�8��G?l7�gn�������Wg��v�3�J�����|�n���:���=��$�j��Y���U6��6���T����j��;s���'��}��,q�Te�-gW(�s[5��*svz�\p��������j~�j�mVnZ�wj�n�3u\�Y��j�4���3^_��Ig~���?P����s{�<uc���o9���3a�^)k�]�L���V1�%�W���������T1���T�����wNy�={�����n�_����9l�����3������\'zZ�	������FI)�'��������'3U���f�X~/^�)�et����
^��f���0+���;Q3N]�����Wy��H��q���P��A�T��f��.�b �}���&�j~S.>�{�`�M���-�P�/�]��*����=��w��s�:����%�Vf�B7j��Ve�d��5�d�j�_��}���}�
������������X�?D��,\��Vn���Wlf��������@��%�X��&kT�Z���5k>
G 
����qU~V2��~f��W��	.����|9^�p{I��=UxK���QwmG���f�N�]YF�3z>�T6��j�;���\��<��%g��:sq��i?�W�?{\Mpd��Fsq�o�
��i��9������S�Jkn�l�$;����U���zklh���GN���������^��m�#��uX�x�}���j����^������	�e����"�Y�k��Z�g{���v^��������2������[��e�����L*�t�S���&�����<;�aq�����4U�s+����s4�g>��yr������
K������nK|����f�h�%��L��	��A�os��	�z��������7�����cU�v4U��3�����l�{E�G��
m��a�V[[WF��=�T�������u�5`�_��[66��^'����3.���u���������v�u��4���xg��q�?o�(fe��5�8N���!��w&;Vj�j��X\j�jI��|	$�E�d��4��f_��}J�����+>U�}C���l_�o
$<�1�n���8(O~�p}$r��O�Ua�����uS��3v��Z������OGn���������x����M=B�K,�`�]�����.����=�<�7�Ar�&*�fUCq�
U�V�`��j����;���~y��u�6:��W*�{$�un��Bf09���}5}��?f�N@NFm�@UXj�����\�Z���_k�����NY��0D��QQS�5�|���XK�yK�3X��zF]t��~�9��9uY3�qT���������}�u��w�$"f����S7�����'��x���S���34y��w���������8���T9��3UM�S�j��AmXg^~���>��OM���UZ1}?5�@��/��u}���>mH�mQ�gRU~�UV�i5�'I[T�U���-U���5�bh��w�<������3�e�d&������)��{��(�P0<�a�d���i�w�te���V�P��f:�-��
�I�������,������X�}�������6�&[����U,���OuV�O����_?ny|������v��T�j�n�*�*�]�&��o����w�������~mu�G��-\�+ot��kJ�����}�r��5��G�&��
QS�mV�d��
uP���}_h_����D(2��y����~��"�;D���n��o���4�7]����}k��>��nsP��&������^����s�3���5Y�9����������
��>�3�^{=�*0�7������2�M��s����w.�N���aDU�;�9�\���PUaQ��������[
�(,����v~(�T�aU��HY�E���-|L����S�nC�)��t��U�B���f��
���5�V�^�����+���(�l:+(]����g�Ls3����p�$����C~��w�<�"����Y~�����B"<�o��{����
<g�
��s��*�����n���8P�����"/{��8�(���>�;�������Q�r���B)
}~��Yle4RI��5h��^`�W�������n	��'`
�����p���E���,�nv��V��g���\������ ��B���#��)��kj���[�f�EUQUaUc��z����v��+��$���aE�Ru�j���(�
�{�=_w��XQ�8�L�y�� 0�������s�c��5��&�$Q`�~�����k�V������y���:���+51�'�\���/k�CIw8��1��mo�L���c2��od�,t��)X��v���	/�]s�����+�����a���p0�'�����<�tU�EPU�}��'��XTU��w���9��+
����"��'6��n������
�����q���8����0�������ZB�0�'6���m{�O�/�Wy
�9��}��zu,��J<w�o�������|�y�y�,���4���s��K��`��� ����s�6�U�f�G1�+��=it�Qt�aS�n�*�aO�|�"*���O���1�������)���]^�{�K���U�D��/s5�iPaTQ���}[�J�}�������J��(������4���E�a��}��y|�TH�6dD���y�����I�/d�Q���keU�Wfq	���G�i�C�$�~/*3~��W�cb�j,����K��L�	3���|.�0�T��n�0��W\��.�o8�z�9��/`��z�
$����Q�R�'t�L�5TF����*�|��(�*�!������}	�!XTAA����>��j" ���(����}�3G"�XU��+{�^��1`aQF���*~��<�l�f�|��T`��}^{��5:����y�t�<���$�+�� e�,]n��t�[��5����u����/�����<a^��
�k���`�r,E�7�jDd�E`A_����}4,'�$�����6aaDj��Y1QQETE��<����EXI|y���}���+
�*"�u�����=���EUA�����'�T ��C���z���*,W��<������e������T�&/����94�Y���z�9KnT�(i����e���vq�Z��E�.��D�^N�����?{
����I+}�S�v����zH���+�(U+nY]�������0��{��yd���TXE�O�J����UADTX5���������"�
�0q���=�uVDQ^�����od��T}�W�{O���"�,'��������q�^��1'�������<q
���+��1C��J���<�HrW����/J��=�������u9vXlw+�4a,���-K}b\MD�I���m��i���������|�o��w�U��a�>;W}L��**������N{{���,B����=���r��PTAA���y���%���(����X��v��=�����#f9W;Wr��""�
=�J��v�-DVQV6q=����4{q>�t���\��Up���N6V���
��{w��+C�H�_s�m�����Lu$g�}���e�z?0�����p���(�sT/�i�����SY����
����s����aaE.�Fg�W��`QTR����o�5���
���gT�����E�G��Z��a�����"�o�/QD[���i��P�0�����UR���^r����};��eF��*����� lMt2��!���"Z�c&3y�y�GE|�k�u�0�Q������TH���5�j�����/�����D�.�z�t��b���]��o9z�*(�=�{\�����
�5��y�m�0�
	
���G$��E=�����J��(���8]s��y��x�ac�s5��y�Z����+WTd��3�7���OK���+J�sQ���V��������j���m��-����R��������/`r��PZ�z�������;�b��1"�I�����r�^&;�{��t�u�}�O�[l�!w��k~�����=����
7S>�����<�Q��A����w7�fzCQTXc�gw�5�;��DP`c�O�o��������|��pTXEa���I���ED�T�}[����v�#����2~����'��N
_U�6�W�-��o?#N������K��^�g6��K.�}yKiK�d��g|�j��c_����~��;��UQPQ^������XaQaaH{���Ng3�)Q��o|��������q��EQE�O������w����=��y��{�d[�������t`E�aC��XC�E�<������a����������i�z�h��`�T�Dal(xef�Y��q7����	��n�"��*""�3�����5V��S�z��6���������bB*������[���=�S�#������������l#
*(������.gPOUis��/CwV���l�V|�+
 ���*(����>m�h�o,��n��/�������,1
���Y���YS���������sF�EEUQ�5�W{��g�'���y�����1
� �����_�[�������o�^W�s^���<�DATDTA`ED���U��RO���M������M�gC�S���glzC �~|�O:7�{1Wr�H��dA;���L���*��V��e��Z�ja%�k7z_$�����w���4��������=����aP�R.���7�b�����m<��IT`UNs�w���n�������=����'��
�����f�Ua�aKq��M�"�������3��������"
���r���p����4�*�����
�����s����oc$�M�����
�����(���2[/�5����ng(�~��v�s���
�#$+���=�7��9�\����y�vx{����;����i�uQEAXTPTDX��/���������P�j��+��s�{��{�����Qaa�~����yd��s�
o�������"�����1n'������r'r}�<�N_s����0�(��(�g�y��V�*Nzk���s��Nw���f���.}���XE�F>�@
�[��z�n����f�J��m
�������"*��s�>�G/�����Cs\�g���p���� �
g����3~���;����su�����P�;��^����5�6�(���'r��E�.N����|%;�����X"�HB��7R9����>�E#h����R���:������mC�K�@|((��z����aA�aOL�j{��30���l�77=���=�v1aa������w�x��AUE�}W���QaQ>������
���s������e��*��0��6Mw�EO���,
��"������t�vyZ���n�qu���}��<�Yu6h�������Uv�if��T���7g�����s�@��GXaPa�.��M���}�Ti��N���
v���P��DA��,�W9���c�����k������zxAQ�����7�\I����y,��pG{R�":Y*�T"���";7��m{��x����xow���k����v��0�	��
������c��oy^N�/��/����M��l�E���
���4�g����N����\y��R+
����,0#
�k&��j���^�
�x��������R
�0�����#6j��/+}S5���#�5�[Xy�����������@����3)nf|�#7P��s���X�����������r�����������{���'Q�fk\�r����_��"�f5z���SDA`a�J�=�9�((0
+r�+�|�����TI��j�v��R
1�����z��Yvpa`Hr�7(E�DT����$`Q``_rV�Y'�������QTUUc�.�EY����N�,��:��Mn���QPXF���]s��\����79�4��%1��h��(���>os}Mu�zfny��	+��i~�����UT:o���1��e�^������iw�o��+B���
"�^g��^;���T��������e�fw$4DAQ�Xa��^r����jT��\�+���5�9�d��HHE�ADQ��C�[�����"As`;��e�t)���}�<�0���+=f}^�W��g}������|s�����[��QEAUa�TX��_t��������)���O��'(��,*����4�CV��8���^w��E���������}�_ �����EdNs�V6F��Kc�3'R�il]�s��8�!`��#�����)%T�z���
�)	P����8*VP��(����e�sd���"*"���v��A#1}�g{�>�����)<�s~�8����=�;�������}�TE�Eg����{��"������=�v{0�"��������w�y��1"��1���]���n�LH� ���#
0����]�/9�5����7��Uw�����3^"�QADH`Gxo*L�
��yN�Xh0��WJ����*�*�0���,4�������n��sn�����e�K�E{7EaFXDaE�W7�{����U�S���_w���XRETaDh
*��Mwrj{�h�����{��(�,
�0��}�$�:���k��w�D3�t��_:�_QDa��Q[�v�g�I�g������fq��'@c�~�����EAT*vy��������;��s������3�����g�0�� �
**+)�������}F����s�������+���c��XEDU���8w���a=\�{G7S��RP��wY:��d��'�t���@s���^
�3,��*��h��P�n�J�t��MC)�u��l*eVt�7��"r1�7VG@��u���lj��EFUAK����l������*�����CaQ�����vS�uV���""*���rg���DAX_�_�=w5;�0"*�7������|``a�F7�����}������� �����]�y�j��**�0"����0��c���s�}�l��zi6�:���������_
�B����>�T�`����s=�YD<�kt�����XPTTU�E���
U��S�8U���Htr��7���aaEVaa�r�9���W�y)�1���������FTQTaE��\�r����g�������<�������aXD}����>	s��'A�%�1U��p����X�7���FVXXc�����X������k�6$�����J|(�����*o��g'�i��W;��m\���w�{_�"���(�Aq�1����R���f�b��PDFE�V EF��5��fd���ko+\sAVK0f������5LUc���%�q�uN.u����RT�����Q�]��'J��'��-�q,�K�����Ww��
X�3���������v�{���XPaQc�e��'�|�����+,���!`a�s��4AAVV3��V�&���
�����+�����<�#��]�{w2ADD%+�3��U��cQEXQe*���1�"����ED}��Z[7�E^lo�keQ�P����� �0����5����;��-4r�wfi���c��;��**��**(��#��}~��+r�u-��]�����������
���P��)�����U���n�s�h�3}���PA��*������6��[��x���]��rQDQ�D���������n�e����]��7��aQDTaEU`|
�����C	k.����a�K\,s�XTUA�=����2M�.�����������'����(0(+
�_{7��V�\��d�������-�#�#s^���]�9GU��=�aUo4p��=�&��l�l�T���:q�����|�`��k��r��C���#����2w;�q���>��}�\jKY���{�v
~��0��Er��vk�%PEDS����eaA*��g�}��*"���+g�a��sAVa���/O��(���u�.yt����Tao��y{�`����+�9���|�
�g���x��<�EUQQTTUZ�����Y���������k� XA��DXX���j��[�3�Y{Nv��w���U�8(�������C	���k����5��U������A�XRTXU��9U�No@�����<�����Ub���o�Q�aDPTX{=��I}����i�j�9�2�n����(��P���v�B�=�����R�:�*����

*��������U���)��������.����DS��^�fk�L����^i�^��{����=t��(���"����{U�y�����^fg�G=s���w���QPUQV��@���}�vr���U�{��l�t�45�f!|��7r�x>�r��75u%�U��r4)�0�5B�c���C��]��^g��C�S�+�3���X��}m�OY��Y����QQHTUr��{{���UXaa`QaS�\�����eqDEPDVQaA�g�N��kk���*a�o�]�g��4��XaFURK�������*����������E}���O#���*����0���}��#�� �� �**���}?V�����XVT�N\�����k{om�X��fRni��`W��>�)�=l�l���!.&�V�!7y{��K����.qP�����5���B�k�6�
�6����������y��MUf	�N�c�zy��t���o��R!�*%v�=�����V�m^�LE���V��.�"��=����tu��3SW!��kk���e<��4��I�M���f�������n�8���A����U<�2S����+�Uv<��N\�W���Z���]��7�A�I
 ����y4���S�0eo[;��e��d��sC��Zz���Wk(a�����m���pn���gN�
���{CuQ	��N�U����������R���a������=#�Q�/���_g2��h���,:�Ug�\��1��#�Ub���e9nu����`/8c����&X��������Ksb��0,�Ft+H�;
M�B(v�G�/�Eb�Z���9�w�v�����m8�&�e�H��6�0�:�����o�uj��I���r/��r���Y�S�D����S�Vn�9��5R���*0�����:��:A�S"�b��*������Bo������h}����j������'�3k���A]�Ty��*����X��Zg�[@s�����^-_R�����W6��R�
���e��Q�z8miZK!T�Gn���9��I��e��FA*m���Y������1��so/���+��M(�����M5x:������G��*��oq�i�`���vkr�Q-��y��$`gfQN�I|RJ��y@�]*&�'nr��o��3~��S���S�Jzv��J9L1��U��=�b�W$�i�Hm$��!�C4�
3V�����u��]�_,��]z�I�	�t����Z�����M�</+,��o�E�}{E������O�H^v_>��x��2[]�9��3m]����X���:SX:����c�&�^��K�D�a
���.���q�},M���
�L l����g������7]����{����k�8��8�E�k��x;E�6������<�wj������@
}���gn�����"0��s7e��k��e_Syl����Nz,%�h3f�>���_K���4�����z��j�����\�git�*�
���Z|��g:��XJoL:&���/i���y���^�R�l�$q���'+�]p����mv��l��t�S�9Y�o���wHx�,�96w^�|�����c�4709u*���9����!5�0����}{p+e�w},Y�R�t�HR4L���j�����Xm�`8���l����X�n��	�
u�[���f]wM���.^��J��P�r.���xm��Ud���f'+{�&b����a_#b���m���o�X��C����G}8�4��%��`[Z�3������%!�^��7�$n:^���X�:��-���;1A���W
19}c&���\gA|{;{�����e>�J��������X�LL���)s`5M�z�*�H�51��s���
WN6R��<6d����=5t��=��_�	�4
J�S�$�>�OU:�+�g+�&���;��M7�r8���^\��G>��;W���X*#�1�3�4'muf���#��$|��p�������sr$:��0���DZ�5��g
��L��y3^|�S���a����=<�*��{�]���^b��{t���7�W'j\pM��5��x�7�'�U����n����Q.�X��X��WfQ���q^l�}5�nv^��	�vu�sE�mf�-/�;P������;�d��D)j���7��Aa���!����mm�\Zu���%�W$��u�\��b=�G^�)�|�>g�����c���n��P4Y+6(mg�fI����*���]�T���Hj@�l���'fn�Q��)f�g.Xj}����S7�E��k�#!��Z��6��[�k�����m3�MY��s����!6�P�}�]��Z=O��s;����������
U��@�����1�����z�K���;�1������j>�v>�Kw��7�������qdn+����t,!�����u*m3���W.�gY9b
�c���|��l��������c���w$j�h��|��JYR�T����8�����c6xb��
��}M���i���tV�'Nh��XSx����p3�G[��<Bwy�(�uH�l�WA�]��di���1�=|~~��j��J}9��T�������R9lj�p��}*��A����L5��G9���m��$c���KM{������dXEbM���@���S��ZyRK��&��=H���-��w��|D@���=����m��;�������%����{CT�Mq���6dr�9MI/`�~�G�����e:�g.�W�=S��Z����M��"���2R�C��d�&�ZE��.��:�
L�������^n�{�����n�O������)�*�M�������@"5�g)h�C;*��/�e#����\m�cJv�����`O�}�J�U��n�����P1�u�8���fX
����Hi�
���*�6�<�|M�o8���-����������F���U��Jt�`�f~�V�v|�o��c:.c�	0Z����?o��J�#���P������u��j���a���*�����A��H.H���>�_���\�^�,����������GrK��+�z�����R�{�u~�h���C���K�X]�L��-P��Gj����'�d�g��U-Uv�W5�:�������\�	l��:L]���VN~�W�uP��Z�n�d��I��������RA}�=�
�(K������8{%����	m���W ["g��� f[+����4~�����;�����TUI���mP��
�Y?y�y��<�[�G����v[�(����[{12Q�������WLB\#�L�k<�`
uP�U�Z���d��uT#N@��|#NO�ze-yU���>~��aM�����nOP�+�dr��y��
�X5����4-�n��������I��]�9��;T;T
�VUe���j�]_[��T�2���U�=u?�QM"�h�*�e
�������;�����P-�G6������6��rb��r���{�M�.�����oy����*���t������mcu��kU��O=;���S�r�r29����Hn�J���R-�
��u��>3s����L�(�;�0���������6eX�#��7�cG�������P��e�Y��j�0�}�&=TI>�T�W������������K;�lW�S�#r�g�4"�;�us�g��������
U`��v�e���U]T�j�{�����|7Q�y���*&�_Vn��q�X�\�U\�DG�&�q��kW�Tf�'�0_�-�_����l=��5��i�u�Y���=�a��Az����]Td�3��+C��{6N����d���N*s���(�{���>��
(���D�d���i����K�n�5p�/z#��}�\����������ww8 +��Dy�.��@��n=��bk� �3�w���7��11�Lw��C����'��Tn����2�{�P������f���Dxa7}�}���j�F��
�o�
������ZO]���-����S!���v����\��y��
"I�$�J�C���t42/h�S����N��T�n�+/����bh��fn�����tR1n
g�;��B����E}��&m��1B����*�Ljjpn{{�qJ���<����|����2����Z�X��;�����4�,�^cp�=�?_����`k���c*C�����T��r��~<�=��}��e�����Un�M�S�uYr�7Z������w����������������A��l��0p�}����7����nO��Aj�f�awU���I�j���jH_�O�'h0��|��]����B��*�r��l�1��%J�7[�=7�E�w����^��u�-T��c�s-�1���U�kV��7O>�O�������x���9;��+���*�/�{v�>U��"p�Nb�hM���I���}���r�am��T�yv�sj��d���c�
�WB����g���)����&���19-N��N�|�'�]��[{����I���Hn5d���cT�Z�j��UAv����;���}y��o��K�[��	'��=}���J��4�Ct���q���~��nH>qI>������j���f5I�T������_Zp>�����(V�����������X�Y7J �����O�?�>)'���3UdmT�j���!��'1T��d�!����x>����Qa_tY?gd�_�h�06E�
5]�AeZ�c���3*�b��mVKmP���T6����7v��~��x��{����_cZ��Vyu���W�f�\@R@�5��/j�p��i_���#�I�iI�j�UP6��m�-�A������B���?	�����C�>9=	UC�!~�4������1�����qo'���U��Vj������9UH����|	rA�����"����z#�Ee���!���	����L����4lV+��A�!�U����y�y����<3�@��a^���%�U�r��f%������oh��b9�B���yFZ���)������ad����B��C�rN�]�yE���Oi�UbJ8�-K�������`oxd�y�����2��W�/�-�h]}Y�2��
�U��I���DU+�1����-/���������y|E�s��O���}Dg��7��7����Z�cq���=ob;R����>�u7"�'nm�=�.��na��l���2�����r���0n���2��
N*n`4��S;��E��gmC�����}��s���A{�HN��b��2�j��Uih�tM������t1m����7(&������|�V��p|�m����o���u�`���,
y�R?#�d"*�{��^�|so`e���y_l��� �g���a���{4q�i����Q��?�n��L%������Lr���+v5x���U�mP6����5��:���&���$+���3?g�H��~�����m����E�	K��#��'�|��_��[V[�`��s�3-P�O�N@
N@�r|4�������bj'�I�*��~`�S���Q��<�|�uL'T'W��j�_�r���k���7Z�+Wn��j��n�� �c���i���6/��������3�-d:%6U������V��4��W�s��u�v��]B��]Tn�36�uTVnj�6�v�?u�������]���h��yY^���=�H$��)�1�G(�����9$�}E9��eT��@�j���`�V5�������Qy���|�j��7���}�W�+V�Mc��i�id9]�o����Us��o�+T����V
����6�j������e��)���z������������)��n�w�����^:U�C6�K�xVZ�/�	6�m���Sp����l�h��[����L��aTn������`	U0�����Ul�:��q����}V�t�.���J_�+���e�-����fm��m�6���m����~��e����U��w'Y�������$7����zw%���>V�U���m�7m9��7v�r�Cv��H���������o�5{�HxP6qhF��I��n����seV�V�����\�e��Nf�N]�'-�����m
���i�6N#����]��^Vt����w�O��b���_vP����������f��L	/�����l���#j�o�Y4����b��_w+ �����~�f�@�s7]�d}UQ��t�|��E�8{�=8CB#~����>	��V���j���U,��>]��n�]�|>���\ ���0��&��[��ow:��}P�G�*��L�Y��u?G��c��U$���G�m�=�zj7F	�{��c�3�������R�>� �v6j���W�}A.�v���}����a2������t/���
��������kI�3e���g���m�������.Q����W�6rz:��^�~��U�����R�}���q��2m��S$72��.�*����L��B��vg��*l�;u�����z������.�����-�m��ov�]�m2��eY�Qv`��qY�W��3��gZF���^�}+A�g|��j�M���9��YBq2�Tu�:�s�R����E<zm�����>�Mn����?��ez�C(�O�k��k;?Op��i�E7�%��-�m�I7����cd��y����wj��g�������}��r�w��AG��b����H���k~���4��4���m'2�K����f��f[g�~����K�W��R�W��w�o�r�pUy�nO������]�U��Qa�)u~�`4���3v���]��m�3m&m�9�mU���f/a�?s��o�������&A����.����r����d��V^�#�z�a��d��������Nm�N]�|�o�m�m����2������'�E�SK(��������4����z{���e��3��5=>i7���c��c�)��M�	m��l|�l|Sl{���/t��������\�~S�
����t�*s�R4Vz�U[����`2����`������)��i��&��{W�������e���8�L=O} ��ry�������.T��*�������	[M�m�i�mn�]���g6�&]����#�{6����F���R~)���
�m@������:nP�y����GM�4���|�l�`��E7�)7E6I�4��g{�??A����X�u���8)g�ylz��5)����Z2a�f�k�B�U���^�I�>-6M�V���K�i.��6�\�d������t�����m�/���[%{�h�CM�����cA-Y�����m��-����&��.m�e�C.�K��n�C��zZ�y��i�"%C)����2���?E�.��^�>��-����yunt���sp���xr<��H=�9��bv�U,D�E����;)G���gB�t:<����{�h=���V�<H��e��,Ud�����C�������G4��h�aT��+���������uY��>���������(��r��+�Z9���D����T��s=}����tK���\��{���E��"3MB�������q�A{>�Q�u7-���l���#��b>��^c�J{6�]�5sS�s��cUia�X�+n�tw������c����P��F��4�7~����;�
Y�JKP[�0�����tk�`���	D�����6v��S5%W�����2�B�k�
�����B,���z9�F�T������������������P����|�{��Ey>��	��U���@�U��w��
�Er�R�n�j3j����XC~g�sf�=CQ����������H��A7t=S���}"oI�����"��n�l�o��c�I�)���o<>�4����z_o��^�*�J�e��]~�^-�]U�-�Y�]��A�e��
M��b�o�M6Sl|Zl�}��;�W?�T��Th��t�p��B{��#�	��L�k�u����X
]�������-��M��M�9���m	����i3-�n�/���~�~�[��,�T�a�)[�['8�����H3��2�[G+kW�k�ZM�S[`�������������c^����.������yp���{7@��:�Z�e��5�m�J~��)�m��v�����hfm���a�m��I�_s����!��LN(;�!�\Z���_j������f�Xa��!�����o�����l7m�m�-���l������JVZU�5Z��1X�qY�����d��m�M�}H��V�w�����_f�a�m&]�3m���Ir�9�i�n�4�������|�����m�����;l~~g
����e��+TT~����M�[m&��[���m���hv�X������Ka
��>�����1R�����B����2&'_2*�8p+o7������m��i3-��m�[l3S�|�o��=�?j�^����/zf�K9����H�o&=��-7���/���%���O�����M6(��$����%6>���&��;�=d�'������a��!T�=��(��ML���Vu]��{V�����9PZ�����Ov���S���<�Z�7�Q]���g��-��F	��;0���� �~�(;�*
�����]C��9y�o(�B�Z��1�0�����H�A�v����������5yO<�����}��}����DO��Lue5�M�ul��=��r�9Z�;��V��]Gu�6����G����n��r��|���#���V����Uj�A<S�'�"��y��+<���>�
��Cum�d���G�4�����b��d�����Rid�et��5�[M3���n����]��U�gy�!Y�q���l�u���x*[h�+N\xC���f���{������{�Qz��T.��.���z�^kh���}mNN����mV��#�����m0������`��:`cv��s����?{�)������j!�nP,�N9�����[����-������1�{��>�`��?+>tRf��;���R��ZI|Jl}M�e������n���msm&]������w���Z��g�0_������C;S+E�����W �����uaFg���m����m�\�I����l���n�n��!K��7��W5=�O���G-;�z�W�|=D3~TB$��qE�V����u1��c���;}��Sc�-��m��m����������l$�����oz��n��JK�������vxw�=�Muu�)&�p�	*�E��@
m��M�)6���&�RM���e��������k��?�<��7��]��o�rB�)��K:�n�re]�\G
�l|Zl}E�;l.m���L�i9��\�Cn��g���@B�����M��qq��UY�-<�+{\���I-6+h��B����{h��6����M��Nf��m%�h%�?O�����k��:�����|n�g?{��ee�l�sW&v;��C�/wO�|Ro�E7���������-�M�M������e3�?w��������}<��g�c�r�������UfF� ��e-��r~b�l|Ko��l&�&��v�K��r�f]�6��>���2�T�'��r���/.3�M��w�7�jj�����i�w�+#�c��a���6����[i3-�v�6�]���u��l�����d����h]�����^.���������]g(�$�y?f��ism��d�����g-�76��m�~�~]\��A���J&	��2��{���)ty�����$���yT5D^�G�.��^�e��Q{w�izf��OK���^���&���G�R���+��n9��l�UZ�F�]��P{�lW��?.���+��t�^Fv*37����UL�j
���>��-����*2�g>;�>
��f�9z�/~��)
��x�>�O��Vvl>��������0�-u?z�=���+�MP���
�{��K3��i���������*(a�R���%����/��fe��oE@sN
���4�
V��@L)��<}������^�n}If$�	�[-��o.�D���b�����6�h����E�w��5tY�f�����wV�1$��&��b���_>9������H���)���kw������iv�
��V_T�K��bT$�.���U��9��"����EGm��I%���l8��,��7�����k�u��_�+�����g��|��sw�3j���\&v�����s(e����|r�k����l�������
����l76�e����y}��M����}7�jF_Y��qP���u~����[:�?��iK���`����ff�!��K������n�}+S�SxN[��}}]�����r��cw{?J���e-$��+������o��-�d�m����i2��v��m�m��������GM�w;��})Lo�m�:�r���Sa���������r�5�7��������l�������r��n��B�w����?��B�"�IN�Q���A~��s�N�b3��~�k�����Os�p���v���������������&������������������H� �k�W��u���h�w����77�a{w���3�|�o��lSc��2���l���i�$��g�/uN��s�X����.���YZ�+4��������y���(6�����o�S`&�
���[`&����o��fa?2>�����[f�^�����)����sX�=61��p\�L�3����e����%���l���C7m2�K�l������i���V�v����������p5>�w���D`�������{����I7�%��M��l��m�>-7��6����0�m��(O�(;aW.6��Q�����i�	�J�����&�0fN�����I��|�l�	6�����i�Zl|f;�8S2��jLYU��Y!�,W�P�f
v ��Y��1�F�Fv�r����I��x\n�K��+���8��WUU�-�����'�#����VC}J ;�WQXC���t5��F`��z����k�V�W.�q�nf;�5Z�5J��X!�U��e/+�#>��q�������z=��D�j!+�[����}���\Um2������[���/���;y���x�td4|�HiC�z#3,v�w��{��Zr{���n�/�|�{��y��k���5Q�wWsu�-v�;�su��b��S5/A�S��.��&h���>vV�O7��&%�q,��s��VHu���o���GY�G�����������+[DM�����M���������A�=$���Y���5}����OA�4���Y7kP�����LK�+���6��Or�
��fJ�S��D`|zz��3����E*�s�u�Z��k�]����H�vx��y=��yu.��9�����n*k�.g3
���+������3m-�@����hm����s-�
M�m��]������/������tK�w����sb8R;pg������%_n����_��I�>I6�l�Zo��l�c��c��`
��UZ�]Y��[+=�������h�FjR�n�V��r�-U���
��m7�i��-��-�M��i������h�dw�o�F��~�6���e')e���h�� ���������[B�1w���E���Sc�)�Ro�m�m�M6M��M���3W�&����c�O����]�+�F}3�3s���v��ru��e~|L$�������[l������hfm��}>��~�������6�����V��������}�aVL���#=�Sb�l�&�����S6��l�o��b�����o���I�����v��=�l�X�_����F��Qy��k�����l
-��&������m7���~�{~����l���� l��G^(+P��5=������u+�
G]+���
����&���wm!���.�Ne���l9v��`�����{�����:����R���c}����K%��X�d��������O;���,�m'3m��i7-�������)�M�Ro��x`/��Ly��?:h@��{��v�u������.���>=y�w�n����r��36�fm�n�S
-��M�S`2���w���A����]��/���7����'z��!d���VeOK"�w���g�_��O}�1�}B����%���<���U_�9�R���zN�{Ou�|K��{2��y�.�������i����4�~[P�J�������\��f�,����o��WV���6r�(�z5���\�PF�N������zq}U���&	_}�D{�5�����_�Iwq���������z<6&V��St���z=+D\������{�������0���G�\��H��|eG�E�`��j����s;`Y�
Z;;`�]�@����:�:�[s�1��!�G�������|hR�����d��[���t�T�|�;�����mm����
���M7��Vd\X�m��1����%�L��nw=�*�FD��<�0�j����)}y���B�c�X�v�z^����	iYU|[���-�Rl�����k�]Qmf��i=����U�n�g%��7?t�������3��S��S��n�*�4a�8��H��y�O<����v���7m�3m����l�I��o���y����7�-O�\��Y�k�S��n�s����eu��7�F�l��awm���sm��m
�����	b����A����ve�������}���=5dT�W���G��^C_j����f[C-����9���������Rm�Z�[�T����������/���Uvf*�����'����!r����S��3v�m��m���m�i��m�m�x����������3����8�5�cxzW��v������;N+~�y���vg�&�-��-�)��`4�)��
������yi�~����n��U�)��m���r��)��sU+sc��\��]Uc������4�	������&������4���l"��>��|�\R{bI+O,� %�N}v��6��T���$F�]o~9��2��	�C�����e����L�i������g>�j�m��'���Vz�����nr��o��R5Y)�FX�WUY;-���RM���f[w-���f[hr����`r���y}�s����������N��5�B��]d��v�/�7�����/*/E���m]�K�hm���l�m������m�����G�u.�\3�i�F7��_z��Bt��_����&����]��s��m�2�d������l���lRm�Kl|�~�{�����%*�!��#�TR�T�@�]�����8T�lF��6�'����P���<��1���\���������
�=���GwB��b_�,�./\`���,�T��L�=���z1���������s�b��|^&9����l��`������������ x������H���>�����\���b�/z=jHr�5c�1QU�_���f��*$q����+5���������)G��[Y�t�!��=�����x,��9�GDz#���m��k�d����{���������0wC�,���P��y��F.'�d\2<����V�b��q*[6j[�1]��Q�/�[t������jq2��v@qu�]��nk���K-'�p�+��'������M�1'����w�v�z)CV��-c
�����L�������5~���}������+���4<��V�}��m������7�kk�GO�;��H��8N���6\jm-���{�������7O#\=m��w��Yz�B�3O|M�m�E�3-��m7m���
��sm'��u����?W�B�}x��!v�~K�o��/(������.�$P�YO�7����
��y��o���b�l$����)�M�Sc��������v��1a����S�r������$
���T]��~�NN��6����|m�	m�I���l���6������a�����vs��!����/��N��K��,w}�{12��v����f���������	���@��@��d�����7m&e������J�������-����Jx��q��z����`�7
I/�����{�m�s-�]�Iv��m��hsm�����.�<���������~��������:����k����b�;4O��;��;e�}�����&�4�6�%��E7�m��m6�o���_w��	��W_�-�89�	}x�����z����;�3p���3U��v�I�>i�m��E6i��lSc��a������|�k���������C���MczH����.��M�|]np���0>E7�E6I��m��m��$�)6��o�[�
�����wV��w&qV���Ptt}-��5�4������W��XY���`����o��hm��m��r�@���������EM=��
���y�4���VL.�� ������r�u�E���;��o�r�6��n[d�m���.�'3m�����|0n�by��HMW�������g}�,�zH���L�������N��_�}�4�=\�.�(��zp��P������x��?{������w�,"��=���Q��UXET��}�3�{0���+yR���6�����UD}�~�����3���y[�TUEQ�7��}+�xB��(��������j
�	�*��	�WR�U�VG���5���c93�������z��b��P>^y�8��z�<���J#���H����k����}^-��3N=7��>{�[''|tu����;�[�uADUaz��{O�������2~�s�[���5a�U!�������Rv��y<���(0*wop�IFQE�����ES�|��|����)=�6��#'�������UQQDF"������]�T�6Df�J����0M{Ae'���9����?f�<���1��G9A���:TZ��%eC���z�pM��o�������<{��of�9����}?���UAQUAa��5�I�"0"(,)��v��?Vw������s�o��
"�y���M�1����w&�y�1XW�z���o�LGTG~���W���(����((�{j���9���kWC�y���&�5���Ydl�-��
:���\=8�1�� � �Fe5�)�#�������$�VN;�������>>qS�(V�~���)�OF������Q�K^���q�-�m����_��������=�@���������"�����My�{-XEX�ro�*(�B0������;��;�6�EDJ*�c�����+
�v���=���EEFw�K�}����@��k�y6��9z�ACz�6�g6�g���~\�O�GB�s	^W���y����W�.��c;e�-�a)L�.�"RC't�7sz�yUc��S/.t��W�����q�DaAQU{���o�:!�U��>�]�s>�M��vE��^��>�EAX_���
���u�r�Rb�0�'���b�QTFE'�����z<�"���j����+-Y�$J+��0����w���
2ao�__���[������w�{a�Q�a�4Ie���_��W}A4�b-Nv#�E�
)`��>�.���=Vr�����������H��((����zg8���*=���k��$0���(���}�;���QDW�����UgU�T~���p��*�7u�>�=�L�0���D�uS�W��
��C�������m�yY~^�������9a��w��o���r�^ w!@�����c}3�������k&��C��y�J_yi����@V��J���=����o'v�_gl����K���;�������6�B�/�}��r)XU�XuNu���O��DXQ����j��M-UD�����5���EEDAS���_������^B�#^������*�
���������$:"�PT(P�%�#�FA�S���C��^����|�S���A�u��]v]������f�����g�4�v�k������}8x�U���kC<���F6VTU<��u�E\ �&���!��0��|����"*8_,��`����<o��{[
���
��2����?e����0��D���>{�QUDEUR�|s����vUEE=�S��QyP� ���
�]c��[�6��w#`)'r/&�����q;��/P��~��[�`��
��\T;[�o���#��s��,��������T$�
���F,M�������'g�������q������@|�aX\���(������9���UEXV�x��\����U+5��v�nEQ_��7;��)�EUU�V�z�2���T�0�"����W>�r�QXT�b�V�o�6�]�������_	.���)�*�'XL�u��_F�S"�8w������~�p�+�@OP���s>
�oK�4�	���gd�<{&X�l�M�<W����I��\>D*�Z}8""�C�eE|���
����|�s��-����l��d����"k�}5��v��DQa�2~7������B0���*y����}�o��*,0�e�ZL�_����|����7�G"���T�������g���$��u��g���X�^�$���9�����WER���##WN=�al�w��6���|�{}�
j����9���k�=�{6������^�9|�]z�
+
�+����g��"�*��~���gz�*�����x���v��UDN�}9����aU!����z��,*
"����k��:"���,{�Q(�'�Q����������.��K�JXK�\���o2��������-��Ue��3�����c~${k�%{u�`�M]��I+n6�X�X��^����42���{Gf���!����{N�F�/�����j��y���R�H`D����,UDU��?s������	U������L`����\�������u�Gp��l�_�4DE����zr���a:Ug��Sd�����8�F���y��V�������;�2�U�T���/
��b�8����X9-����"UI�;��sr�d��������r�TEX��+o�-��",#��=�}�����DU�W��PrE�r�����<��QES�����9*� /k{��{�+
$#[]|w����PDQU���n���J�������b�=��c�����Eo/����FEbTb��@^�oK�vc|��`VWfL������AQU�Q�aG�]+g��p�%U�Wk�\����XaVEUQaL�K�3+{�T,v���]TVhZ;�,�������**���Y�'s�W/9o������7,�����
�A��J)s,��t��f�z�T�R��V�UAA!��r�����tlf��<�,���Ds��("���p���r����d������������*���,��"��zY�=���%�c+zp�;Ij��
��**��**�a<�o=��|����v&���-���a|(�vgeR[�Hz���*�����������o���<���Tt��B���ns�����d<K~�$���{��y��u�$�b����x���������
�����&���2EDQ�F����~��z�","��Wv>�s��-�F���s6����w�B�
�_W���e�UUQae�z�~_[�%U�Q��������(������W��DQPaDF����������tZ-0���
�qr��w�|>
�+��
B=���	��4��^�:�z�{�x�z����*�B��4���hU�����Z�j���kUaQVF'{��f����f���9\�g1{����Aa`QTTQUa�y�����0�����O�����fg��X��"����73[4Mt;:H����)���P�,#
��y�8k���*��J�y2�vs7}u�|uEETUF�"d�|jX�c+9��[�����B�a!XF����\����������.����&��$"� ��"���[�~���������y������
��/bZ��B��e?z���jdO�j����T��9P���?_v��L^��_z���uF��0�5y+3�g��W��a�P�C������0�(���=�6����X��<�r����H�"�������@�***���eb���yh�����I�>>I�7&p����"��qf�7B���*4oeI���,""� �*�S���s��I��EDFU������{��w��y=������RUV}���s�wJ(q���f�����J��*�"�*� ��/.i���}�7<��������QaHTU�7�p��y�f���T���n����d�*B�0����
����O�\����#^���fETARUDC��W7�����7l���{�S�;F��R��#
0,
�*@
�9O�u�q���mP��`�(����#��>;���.s��{��wjo��������XQa!~�
�K��������	ue�x�-u�� �*�����D��&�{��,P���������i�F~t�2��{���u�X
%���M��C�+$�yrD,����^t�
Vl8pj��:=-��[������������l+

/�5w�/9�;}6TU.���
w���EQQ���H*�*�f�{G4����**�7.�{��}g�"�0���������aXy�}T6`"��*�L��]�k�DFDUa�j{{���p�����M�x�Vw����#���m���/j��u���*s(�����(�����0�*���=�WoN_���Ug��{e���$,������*23&L�u�j����S���9V����QAUTHDPL�����y�
��'o��r��vOoS�DX�aEQE`V�w�Y=���VN�t��'r�2�;&��+��8uQPVba�����s��+��v�J�L��a�x�+���v��NB��0�����
3t��o�{���]�;&���5���q�QDFE�=�yk��9|�g;��U��L�u^�B,(#��
"�
>��;�-���9��k7�Md���+�G}��2�H�,�'��g����fm#��^=�n��9��w���]@��X�M�����v8`��s�K�Y���9]�aXXL��.�����"��>��3����*+}�v�~�*������
��H���*�y��������Q�P�d���e�0�"�&|��������gQUFVf$8�}aB�����A�<|4��VlT4,)X���{�l
*�
,1�=�vc|I����w�x����W9��QEPPaE��-�-�Vv�'�@�D���w5��UEUUEaQH�}Z�\p������M�T��U�(�����=}[����}X����������������^V�9��`V.������%���I��/�N/}�P�
��1"����O%������W��w�y=��t0�(�
U\h*�R�K����3�3m�z7�)XTD�Q9i7�o����6�p�>=]���������'�6�$"��1�a������t��J�c�a��Z��OL�z�n��'��<��z�'�1N�����mg��6�������{�~��Z�vvz��)^�j��S�hi|7��PzruC������G�9��UC
#�z����QaE@M�^�j&QDW��}[>���c�(��=�m�R������EDD��W����a��PUN����y�����aUE�����j%UXXPaX���'����~���#r��.v�q��\���M��+:Z����" �']���,�k [m�g9`�[(J�7>���E�wg��fr���3�x���4����������d0��V�t�$�*���J������P��f����"��,"�����p��cq�i��w�v�7��(�
"$#	 ����krlWB+����R���H?U
�(
VEaE����U�����]�m����5oou��VREED�_
�>�y�0��,�����N�r���(0��*�k�����Y2_:���K��c���ag"@�AF���u�=�d�������L�ma��K�6�I��n�ma���K��_<�gZ���\,���*q3��w�$w;�O[�|�l-^
�*�k�_��Q���R�q����Z��;A(����
sf�~>�{������.�}����E�Q��������BI�+����y�rEUXP�����O9�"���k��s>�X:��0��������� �
��������O��EaXFE�D����y�~���U����]�=t>���{������*���
�1>�]����G��[��w���>{�;��M����+
�
�0"�(����+���2��U���7b�����~��gf�C��� =�6^O3����;����{��;e�{|�8s����V�DEEX��{�ko�=��s�����i}������*"�,(�T�.���w,9�M]����coZt��@RE�a�ss�nz�c�����Rz���]y��v��"�����We�
8d��D���y���Kb���l,0��"""�*{��c���r�����{7��e����*CW�U�#�wv����b�rp��%���bZ�|r
��n�"��)/��k	>>�4�������g����]Uw�Iy\d��]�J��{��z}OEO�>�V��r��yY�f������(����~�ks�y�X��1��x���h���
C�|��XQPQ����^��#
00�����|��0���7�~�����
*(��3��o!ARf7�(G�W��*ADTEw�Vzn�8w;t9����ofx�("�"�*)����s�|����^��S�;f'73��RE��
��"��S�zn���^�2�<g+����qUXFDDY��������_W�����{������g}`����"�B��V��
���*f��]lk�������v�XUXU�VQ����,����vkm����*�� ���3<�|&�J+C�lc�AU:#������=b�
,,�*0�;{����{'�k����uS�7#W�][t��%	BJ!V�!;�S���{����o�ur�kw3�W���'�i�V�a�EXV:3��a������s;�[���k}p��!l8�GIW���x]��;�#�/�����yy��nzf{����N6�]q��Y���W���mxbL��7H��h\������q�b����V�p,+(�'��}��3�%a�DT~s>�Wv�����
��"+#���s
VaDV#�L��Z
�B*���0���Ku���l�DDQ!QETF�fo��1�TH`ET�aP�o>�*�aXQ�QQVAQO�E����Q%.(T ��;���������(N�W0��a��"�v���������o���D��M�=�>����uy(�Z
k
�v�j�Djv�;{�>C�5��&(����������(
��|v��:�v1��������$�����{*��
�#n��,m.���|�g��S���m�����,r�n��������;��t�6������7,d2� ��t�d��wu����r��*�WfU�h&����G�w+�2�^�Jc�T�����
��U��0���
���AF��a���`�����5p�5��������:���"w�{U�t����c��/d>�>A����D�������(�(�W�X���2WIi����v�	�����h�E?�Jj�Ga�62:��.`�;om1������HR�30+�"C���Rwu���3�U�2[SkA�x�ai	ia��WQ�(#����[SP'S|��5h���n
]�r�l��r����hL�g&���B�U]����[����J�+h��nZ��������t3u]�m�d�I��$��3(=��(,��mI�cK$��V*������W[G�M�7���G��`�{2���>�����\$�p�a�@02�]���F��|�o:�2�L�,)�uH������z�������2�V������Z�3�lg��sj���)��\tQ�;>�<G!�^`Z�zue��J$y��I�L��I���7�e�3Y�1�3��6�����Eao8]AL;{�8
3:���	��W;,A�Lj�M��)n�2��P�$gCy�
���t���_mRN��3^�+t�d
i�jw��r{H�����8����|�ky�7����uo1}����@��GR���i�gQ��Ehzo$���Z��
�������G>�y��AVQu��WY���tW����Ms���+9@�G��M���������\�Vh�
��+Ze+��|]�*���UM���{FcZ�i�/{+�f�HT�{\����	0,����a}�J������!u	�Y��9l��<���`�O��1&�����C|c�����e�p_^�[����oz��Bj\<�m�J�

B��(��c8��\�P����S���U��������4�;%U��V�%2��x���<\�^���j�}r���L��-}P���?u��-]bh��:��"}�KK	�Pu��rh��w������I�A��3#e�]�X.�Eon>�q����UN�9B���T'e����/Q��N��O�;��VU��J�/�7�d�9f��otS��Yt���������#c-�,�N���u[�_vrZ��B�;#=+-��44riAR��k`���\�K��&L<�N�w v��W���r8���^�C�a�OS�b�
�3-Xp��\�q�	-6O;�����^���������t��i��E�n-�u�'$����J�yd4�r�)[�G�����oJ1qe����6t1�����k=��n��:tb�}��0]����&7c�����.�AWX2�3����T-
{w�!Z�q#r7p���������X���\s{-�,X��FX��`��<}}{����h���}�h:@�q�l�-52tWc����x]
okK����N�^o��Z	X�8��i��*��]j%����^:n;�HU���PA���J�S��8TxCRi9|��j��H�����On|RMp|��+��[�1G�C��<S9�7,��Cur��B	s_�������6���R��f5X(gQ�$�.�xg[��9�Ef�i�6W��+RU�tg���%K��TV������KwF�iia���n���1\]���J����]��4�M�Pr]��\�1���}�"������=;�����|����1��<%'�,:_�i
V�E�Y����
c����7�������j�d��
���Z�7|�J_4����gg\����D�r��������U�u
!5�.`��fu��)��t4��X�B��+��w�_^S�y���
��1��q����e<��lrWkN�����R-�T�r��=��Q<0�a]���Qxa�=
�r�;��T�M�#V�������rr.3����;!��gS�����)e2���	���&�MaXr��;�M���T�X��z�O��p'��i��bM��]���.����^e���]G5t��y����8��*���%
�������Z��f���]�������X����K�/;��e�^3��}��_j���:9��2^P��L^�b�c���=�.���g5��"=�g*yD�1\��3����:�j�9�����x�X �G��}{V���=���ea��\�.�����Af����922=�E+YNKk�\>��Yh�`3�2����[���lf��9[j�i��+��\]M.L�K����l\�s��-(=o��`Z3���&�����+OK���k���������m�;st��sr����8�4Q���6ZB����������eC�
n�CH�Y,	~��[����y��
r��)9�w�2wF7q�S3��G��n6�\������^^��)04h:�������4���=�T�]dY�
�v]+��*�~����X�(M�z�wZUn4q��}M6)��M��M�)��`��&�t��b�����	��t��Qo0�ba�����*�=�{)f�Z�;�����y��m&e��iv�2����-�M��.��+��������N�^`��wk�Y�lX�U_�^U\[X��M�IL�C���1���2����6�M�>)�$����-�m�/N�����
��=]F~Y����3�������;�9�W������-������6��QM�M�I�M��e���Lz���O�~gc���n]�-����]�a_h�4��C_�����������l�4���[&������-��m�<;G��go�qU������Qh������[��Wh~�QE�����{V�
4�~���`v�-��m����m�sm���������f�|s������%�^gab��Z�8���{���_��l���Y���}MZ�ff�7v��m��m'-�9��$����t�^^�y����]q�D�������7M���Z��\��A>�V���he�^��M�>i7���`Rm�m�Jo�S`Sm�S��$��R��_��^]��4w>I>������Y���t�(B��k���o�|Jt�b�l4�����M��c��`6��������Y�������*4M�S���c����*F�p4�����)��X�4����e�m�m��i��m��o��c�{3/���:2~�u(�W����oVw�m�l����v+��9�N�hR���[UC`�Y:������p
��k�V�7�n������o!qy�R���1��F:�k��#,g��8���@��S7[c���Q�{��������<�'�X�B~�z.�H�
L$.���>U����}���
���xZ�	fl����
>��u~<o�j���x���"����n\�T�|x�����}}:���>by��m;��|�6���������/�	��{�0x|>���/9�)���^�U��-�b��jz����Cv��C�J#c�ZF����z-��U�s����������`l�2�x/2��������G�W�U�e:�V���ie��s�����tU��;h�1v�����k����������j.��s�������1�C���M��mV��Q��H����;��M��|2{t���yE,Mu�����0���AoAWt�v������N��;s=R����~'����i+�����i��y����M�hs6�7-�m�[E6m�
M�
M�k�oW���Gr=��j���������(j�w����+o)�]{�5�Fu����0�t9�i.�d�m���6�a���f�FS`��yX?W����G�]��u�:���,W~�i0�X���T��������������[d��@�m���\��m�M��^�g�������v���fW>��:������!�d�w=�z=��������n�Ne�v��m!����c��m�������g���?>r����k_GO�S72��h�[�.��E�z?L�.6;���-��lE7�j�[m��l�m�]�f[a���o�����3�7Yq�o
����rwu,
�������gV�L=uz?E��`��-Z[l2����I�m������<���B�.e����;���H���~B.�)j�t,��g=����q���lS���
�he�K�i��76�s-���3�M���Q��cz��iJ�
��.w8��<���R��C�c��OU��%�i.m�����m3v�r�e�eU]�7$*���6���l��9`gJU�n���xkA���X-��i?����i��N]�����]�����aI7�Se�j����<���������{���@���u��?�D�^�Mj!���%\�����i6i��l�a6��o��o�S`�`%������Jm�c���k��r�~bU=C���Gb�:������J����{��\t��=��z����S���S�[�C}�b�}�s�p���w������L�R�2{-]���m���??��_G�]W������V`:n,��J�����8+��������X�-�#����_r����l�����9W�f��&���~�W
+�
%����J5�m�v���7��^���S)�q�q��X�G�BI'c��p�P-�<�n7dZq�p�+)<k�0[o+)WE���RnzO�y�n��~������g��[� �@Y��������b��fD���z�wF���f�
�����n,|1:]	��,f��N�3w��~��]M>H�BKG.;�Y�X=�FO���+N,,�T�c�=E8u;p�M��_���,�W�����i ��7�"���^v�������:�l�y�a�:
v��<�[�(�(������q?#���Z�}�����L�������5�!F�S������mn�36�[�����m����i�)��������a����\|��t�LTB��P���������q���+�-����}M�-��)�6���o��l���2�
&������s��J�������j"C�5`^������B������&����m�m��m����6����&�S`$�.���K��e/�
������qa��?���3s�p���i����<�V~wS[I6M��o�S2�Rm�Zo�g������O�����2gpq�s�T����"'vR�	l�rNK���o�������\�an�����d�m�3m'3n��.�7T�M�L����in�����ggA�`���uz
��*��� ���w��~��RM�Rl|Ko�M6Sl|�l|�l�b�lO���������Hw��'������#�?��B��Ro����X]����n�f��l���M��$���l	&�&�WqO���������W���f��������30�z��2�vU�9�f���o�Sl������2�����c�{c��g��X��������5�����e�u~L5�e�U W$��������m��o��|�m	�m'7m&]�m�r���������0_�z�O+[7�sy�p��#�����9s�6E4>���|M�M7���9��r���m�m�2�m��>�c����F�~�i��J�Tgg���Y�2�9)�y��p����z7�T}z��#r��>��]�3s�r���z
}����O�����2��^bM����E�B�fV�c�g��.�3S���(���P��h�ed�#"�������;&�@3oK�k��G��|�U,����D��g��o����2��o�����:z�~�����e��UmH����z8������I���G�jS��3�(��y����8��1�X����Q�<Jq����y��=����gr���,������
��e-�8h�>�M���������z�m3���9,��o���#op�M�c139��+"Of_�%2��z���1:T����e�������U���kgg��Y�;�s�eqV�%hT�]N�������]]W���mF_a����G|�L��2�_�*��1� ���'���"��o.�T�V5�����-�����~2N��}�r&^������J��L+R_�G��hi�t]�(���s���[`Qm�m��6�M��	���
��wq���)���g���Et��~����&#���5���d��s���XW�����o���`$����"���	���c�caW<�O\��Y���i����iS�NAb�?4f�He�����<{?s�|Ko�m�	M��6��$��4�m��I��M��I��������������Zfg�b�a���{�~����8u�Z%[�����)�M�y+l��a���v�e�B��n�M�l&���}g�c�N~����3dg|�sm�Eu��.�}$Y,�U�����Q��<�����o�Ro�
m��&����l�`&�����������^T%���������rn>*:3j�ijFh-��2�%/�)*f���
I�����rnm���I�������i7-��y���������z���%��x;�=���D%��(��%<�����������4����n�g2�2�a���.�-�����nH3��������&k9�/t$ne�x���FU%�X�R���~�M�n��hf[I�m�6�2�m������|a����h�����Vc���w�����1vp�z�PsaWK�Jr������[i�m%��M�hn�a�m����m��I�4���g����S�S������M��m�d|��Ik�U�/���(������,cs6_����l�����M�l�l9��m�.������}��W���W>��m���Wf�U�t���uuu���r�x���i���1�kln��.���o�\<>��L�S���U^����B�X�E�^����92����~�m����[U+��!%5n�`���-G�,�7�.z
�3������������q���t��mm����F��z>�{�����=��DK���v"�n��WG5f.����f{m��.�:AC�����;��=�|kjYO��3�f���O�|
�j:`�6���
�F`q2�aJ"=|v�����s7��@�J��{l�J��z;>3����Dpxm �^t�n$�LcM��"�����{`�0����K����:���wd0�o*3�����7�1��7"���+�q�{�o����b��3|�1�����������W�*�)�������4Y�����7c�5�HZ�:����+���^y����$N�
�����D8d7V8i�'^�0�G���=���	jg4�A���Y��K���L�g3*�~�����o�[|Rl
M��M������]{��j�������d�����s8��?I��R������q���CM��m�M��m���v����6��m{���z~��Q|�����l,}t��dS��0Q}�F!�o_���.����[`�&��6��m�l7-�f[d���T��{R�	�?a���Bb��y��%=U���/W�(� {�
U��}�|{�9v�[m��l6���m���`I7�������S��x��y!�}��N�g���Uv��J��Q��<��m���C6���\���m'3mm���`[l|Rl}����_�N�~�vq��9����u�_�N���{��r��}�m�awm�2�fm�n��l����M7��S������~��v������-���4_����Q������+��^�w�[������6���N[l8�`������������v�������hW1����K;L�(S/���=uGNR]��sv_�{�����&�i7�M7�i�m������o�wn����,�*SK���:��;�b���"�h�����lo�e]��bB�~��������+m�In��n��
�l�m���^h�������t��s]~����=E��w��t��1{/	������E�c�����l����i�����n�.�d������[>���}�_��s��C��<�>u�����<}�c�
���{[�OV���D�(������G����.����'S^����u����
e����Wp�$�:���v�����$P�m���E�=z���C\�-��u��J)�w�_,6o+��akt�gq��z�)�=��j�U��w��Dj��;6#����f_��k{T�vk��x_Y
�(�����6EM�<�q!Dz=G��73=�q{A�Q�=l���vZ�}���y��?J����Zy�U���"�y���^���� ���
����}���LAwmsYa�W}��Si
��&���x�L��;q�6Xx��BR;�~��6��[%��V�v�B���\��������Vj\��_L��z�+����bi��NW�����e�W;q�R���
lWQ�����B���Y�����6��NH8��gE�u�!��\�����;�C���2e��Y+���y��{y�������ERN~a���Y��,Sm�M��i6m��&
L|Jo�Kl�`d�������r�f���^�z;Z?v~�#7�������I�b%{����ZE�i��&�	&��`�����`SM���nKS�6�~�d����jo�\�8����2�w���GU�{�wu�w����i��I��m�M���-�Km�3m�l:�����������,��<N�����C�[M��uvw�~��e��
x�1�����
�?��
�B���i6��r�v��i2���6>�����G2/��YZ+?]_o?�S���c���,�{}�J�oE���7m��m[i7v�n�M��m�a���������Zt����,���Ld�N������skM^|5Q��Z��s���E������M��
-��6���&����������{�we�����{�5��V���������Z�{����j�28���c�4����0)6�
����B�n]���l�[am���C������v���}���������:
s��Q��]�X�uj�^������������������s-���I���������?�As�w�s	�����	_B�J�c���Oabq���KP#���b��������`�`
)�m�	m�
W~�w�
��{�k���X�����M�zY�ou��IW02<��7�5�7���N�{�����$�
��������[`2�������SM�?`��������%t;�T��[��W7����*v��
a���pT��*�n#2�
���F��_EF�lA/{%|�P(zF�O��B	un��`��W��C�����!"�{W;���@�o+��=,Pt������Xq�w����F�����[�TFa�w���C?mPPWA�>,����B�29����G��1�j�}���k.k�}6�}�-���j����shN��E����&��T�sF��F�{��f B��F&����<��F
�\�']��mg���B�S���'n�]�t�,���)����l���z�us�5�p^���M�s�ef��W)����g�:�����O`qf8���y�2��egPp�s�55k���n��xR��el;`\wp�����j�YS������Ar����7�U�u�����,����4J�=Px'3�=���>�&<��Qd����A�9�G���XD�.�X��B�w"��yA'���?+�'�U����5�jw:��?���o�M�Jo�S[`6�"���S`]������&B q'�}�s-�{[�b��6�����>�_}��L%�z�����7m��i��wm������o���e7����n���Q~���	��n���c�����`����<��f���>����,���r�9����sv�)����Sc��c��7�u��5�m+3S
�gQU�y+&`9wj�OJ���y$���_��������%�&��������N]����m�wm�;��}�fw���j����:,��F���j�����6�f%��Vm��}��Rl|[�M��
�h��[l�m������2>��]|	N���1�M���v���W%[��{�����3c����Sv��:���i9�l.[d2��9�l�v�v���������������[�l�G���-�����V��PQ�1�m���7���������������l����l|RlSm��%^������~����M���S?��M*v�v@ �v��e���tF>e�-7�&�[m�����������[h�����5X���F?����3gTa�8���*G"�����z��u�t��O�I6�o�V�3-�sm&f�K��sm��m�}����������������O����c��*���Y�l'���z�^^Pg�Z}��mf����m�2�'7m��i
m�I��o��<�����������\��nvN������������H�j$��qF@f��;��7���eI/y���6W��X����D9���v_�]�Z�����p�"���%�����������Q���'���W�3�n!�M\�E������3UD��Q�+����W��0�S�z<��t�6��%�;����+"���0��D{��f#�l�.�������m����yo�����y�yv1>f����Oo����3��YU���\��h�o��=t"2�����fM�cF�B�����3�5��g'2���Z���"�WHns��6�y����{�JQ��{K
B5���M�EbG���w��*fk�+x�]�����#�
.� v�q(���������������J��1Q�O)�I|H���]V�*�Q�}v�$q�qa�<v���E�IYnvn>4��S��}��uoyP��B����]�3I�]K>U:������r��R�o�����7.�V6mi�]2n�M��6�d��I���r�Zl$�Sm��l#�6a;��G����C�|��/�������3b���/��o=��36�e�`n[v�I����i)��E6>E6��pk�51���b���]�Y�
����P��6t���F�F�����������������l}�m���]�Iwm�m�s�������~=�\~���-O`N&��b���_I-\�uV�����kr"���=��]����
���m.��]�f������������3��q��v����|{�:���r����>8r�����m�#j{�-�)&�[i6�C����l7v�nm���H���L5g��j����d����V��3�l�����}�yI�J8�������V��l���3m���v����v�r���&�������U��\����%�|[n�N������s��}��	2��T��\��!�mf��l���l����k�6����}�/���1a~��S��T�)�Y����u��pX���;�!��_�	��)6���m$��6����M��b,R����-W��T�D�@�}���W���Y�
=��A"w�^��o�m���d������f��m��)���`>�k�#������X7_0HNz�/�^�l'<w�Ch�
��3�����
���6�%6n��l3-��m�7m'W�~��{�{~���:-�l+u������w���z�>�Y+�_b�/���l�~��]�����8������u�P[�q�w}V�K�F�tx���E3�z����c�L\�v/E���;���O%X���
{���V������I���f���=el&�dd�U��s�J����A�����S�,��alU�G��-`����c���Y�"Ev�
�\��
Q�G�DS���s�x��+��A�;���buL>R_����N�d�d��8�y��6�s��!�P�]v�W9�U�0������6���;"���4#���q�����<@#]5����G�����2������r�������K�����^5��{7�K��Y��u'c3�=����f�2VR���z�D�����3�$������Ji�Uf�t<���$�.��������6�f%�A�htKc*���4����;]Ia�h��WS�R��9�w�ax��<]�u����8F^u�������w���cwt{g[��ws�����l9v�36���������v�)6>%������%�{�����V�
��3w+�7��4�!�����zafX^�o�~���sm���Nn�L�����m�i��E�M�����;����iH����^?gg19S����9����
4"�B;��SBV������l&�����c��6��"��lRo�s������2T�y7�m�;������� �n�q�r�������U#0����M��l6�3-�v�]�r�a��
��s���������{��o=��`����s�c�E�^V+s9V&�B���r����y�m�7v�]��n���d������������wtvx�O�.���]������N����~���9Rn�}�.D�/�={��^�������������f��m�>��������u�i����;�I������k��>�#a9�v\�X��_��m��m����)���
��
&�
6����m�����j��#�:]=o��jzb�7����6���v��������Js������E6-�sm���nm�����h]������������_�(��">��e�y���{%"��'q�t�i��?��fn����me��7m��m��]�Fv��T]�~���2]�h��K�N��g��f[W4��3W��v�3v�n�3m��������m����$����f�x~^�+���Y�_����Y���3c����@���p�^q�b���1:&�Q
��n�wg���c��ue�����t�RlE����������'^0-��nlcT)\�vg���5Z�r��2������`�"-��w�v���w'��������`�?z�|�=�*?a���>
�����O]T�{���k������3i{�����%,Z����{��V`��o<��3'��z���
�j�l8���>����Y��G���Y���w�(�zaofF�1M;��	C����t�����#{��zf}���(����:��Z�>�>j-���[V9��I�4'3[�f��R�j��t��v���_p��j��{����,�T�d�������9�1�����<gyN�?K�j���hn����������=�����5���qi@��M'�_�[�������0�u�W?���E�>e�����w���,����-o�9 (��h�g9u���:�G/e<*�������M�[l|(��I��t[�`RM�Ko�����K�W�We����������y��d�<n�����=;x�M�����m�Ko��o��t�`[2��$�������'��p^w�rh�	��=�ym����}������u���������v-�;�?�m�����i��
��\���i6�~���V�M��o���f�P���;���"���b��[�u��q@�lmvu���7m��i�m��h\�Cwm�M�m�Rl|�������4t�~O�D�������pKk���(MS�'0z�t}E6>�������&���
&������	�����u9���?5���a�.�Q��_�G�LyJ��~"�.P��k�����4���j��m&��[l9v�sm�.m��y�yN�����������1�����K'3�|����A\��2�v���9�����d��I����m���7m%�o��o��l��P������I��c;���fV��{�mD�bT�7���p=N\����I�~���)�)��-6>e7����&��$��QM����^�T32O�]!
��W��~��o�m'�3eI������4Ng#=�Sm�m���7v�s6�����������i��{�����#�L���.���X���F�f�ku6�7�f�P�����M�l9v����r��f��l�m��������[���������J������x?�7}�����G��<~�Q�����w���g�P���?s�y�?����?(�>��������� ?\O���w����q������~��B���i��;�n+��Q�Y_�X���?�����?�k�x��N��?��w�s����I��?�y���� �]���?�"?�?��G�&q��I?������E��~"��������#�P�����C��DG������w��?��m"���"���i?��������A������������������������������"���?�<D���?������?����e5�
h��A���Fs��;F�em�E���
;w9�������u4mmve�.����m�T�Jv�������Zu��wa�54��u�'su[m����F������-�Ls���v��	���vk��m�q�)Uk���v�����N�;Nu9���7Ww6��%��TU"�]�t��s��v���;m����]�i��-;n��vd�v��NEv����\:[�gwnl��7wZ���)u,5)R�
�]����aun��f����j��dK�Zwm�9�u������,������5�X�U��mv�Wn\�];��sS�v����;�n�Wgu�gnwGZ�w[+���I�S���:�KGv����m��UU�������T��6�F�����v�l�Y�\�����Wm�9��6����V]��%��jH�����P�weR�wi�sw;�N���N9�n��c{w1k����������+6�����2�w)���;�l��B��c�v�:b�m����F��wl������wU���+-���L[�mlk�]�\(��p����v�f�6����n����]X��w:�����hUkv�wk���l�u�v5��-���vn�.������:���j�k��Q]5l��[]wUwv�v��uq��m��c���d���l�k�wn�J�K�h]w0Z�[�N����k�v���kwm�S���v��[#l]�,�����������j�ja���q�b(���m����R�������m�r�MHnn�J���C��Y�nm���;7v����::��'Z�r��v��n.v��#aQd�Mvb�t����v��S��g:��Q��c'Vs6���]��n�������nv���:�M�
Wb[I����.�������K��\8�����mL������v���4�m����.��������-j*�]:��qN�m�r2���-��RS�]i�TM7n�&w6��+Z3B]���u����2�����*KV���Z�gt��Lmu���d���N��f��������7t�R����KVl�������X�+�S	�wq�w�����m��+�ss�\�]�GM����U��g+��b*����;���;�l���.�M���Z��m:�n�5�i��w[����U��k��[evu�ww;qn����m��w\���Z�v��-�[����[l����v���n�N�4j(�3l�f���N��n�vl�U9��6n���X.L�wN��32�[�]�9�t���;��[;������[���sP��sGV������u���m�sl����7s��u��CT�7s���;��l��I���.�6i�l�ev��l����[�K�Z���2��q���u[]�n���)f�n�v���wd�d�.�-�wp���E����n���63v�8�����Ut�)�ju:�f�f�iK�����l\:�NGf�m[u�wv�`��:���l�
����s�J[wf[J���v���n��n�N�6��K.�%���&�]�]T��3���s���kkj���lx�����
��6� X��w����:�44��J��G;���[�j���v��j��N�N��Wu���te��km��k%��BE��[4b-5�,��E,j����H*���bjS0�+M���
mm��*����BF�hPkKU�2-�M4��(*�C
���j��m�m��
PUk�lXi�C�A6[6���)��mU+6����R��op1�spF��{��\�pv�pZ�6]��m���z5����:�{�ht��k
5�Ez�;=�:��M{���MiBkM*�5 ��5�[)l�b��Z��kl��l�6��xs�fw�.�o�v���e�m�����g�l^��{��K{oc�Q��}n��iof��f[e���s�[<����d�k�&�������oe������������^��|��z��ov����|1��o}�����%�[:K��=���v�v���������m�������m��{I�n�i:|�r�}��V����������w{�����������{������������0.���CC[������wJ:���t*�����J��
�hG�
(�����������fU�K5������$�V��n�{���M�����o{^�nEou����;�-��m)6��]>��������v��l����s�z���m��-��l�v���\=��w^���-�����{�v���y=�;]��d��2������vn��y�f���v��]���;n�a���^+o��{�����[m��:���K��}��[-�.���[�����{�-�����m���w���p;s��w�p>�\z�]��������pw��&��u`zWJ��S�@��kCMg0P�l��N�9�j�vJ�6u�N����9��$Vf��U[&�@M����k=�v���+���nM=�m������}��yZ���n��m���{�v�oc�����Kl��������v`�}����m��;�m�����m�K�z�l-��m���K�����F�}�i}��{Iv���K�{�}<��}�m��������R�������l����v��Y%�����i�����X����{f���]���ol����]��{�v�v
�r��}�x@����F���;���]
zWz�������t���:��Q�������p�kL���iWV5C���zW�V�[e��+D��L�eT��U�4����Xw�����v�]�m�l�^�������w����������lk�t�����gm.����������(����t����.��������O8=��o'������n�M��@����������.^��m��g�u�{l������wn�g�(=l�w<������e��re�A�����oe�����7f����:��og&����7f�}��c�_����.�������=�\w����a��z�Uv0(t�Gx�4*�jn�t�+V��z����:^lzA��������.��E���V�m��iIT�e6�"H��{�F�|�����{-������S��;�O�=�]���>�;�v}��T/�Mk�v{e�������{7�G��m���l�>������=�u��({��������������e���Y�rk�����wf��I6�z����Uy��wg�������;f��w���G��������e�[<���v��b�o�}�vn��n�.�����y�f���m����7f��}`^v������������<�wn7\j����
�u3J�����JU��]h�^�P���P�5���P����O[�p��j����l��Y���ke�
,�-iT��/,�)�wg�7g'�=����_#�>�|����n�g���v]����6���{;�v{�vrI�I���^]��y���{6���y/g�7GG��f���������^��{6�s�z/f�d�������7f�(w�v��g��g&��f��og�Gis^{����=���$���jz{c��=�{7�{6������6����G'vm����{7�s��@n�/��0�{���OpF��X7��;{89��k�
��T WJSU��5Tm���Gz`�h7]�P�����P�m���6k-eFEi����Z�e�,��I3����7_�7�{7c�;:=�����y���(��4wd�� ���y��:}�w�vv���6�A����{�{7�����]�gl��;S�������������/g�f�w���e������=����'s�{7�{$zv����oN�<q7��]�.��O{7����]g���;gl�g���vIs���^��������f����}�y>������#w��p3���=�`m���X;���^��X�U1�AB��
U��`)CZ��=i�n��)�<����Q��u][2��m�Y@���-�Sm*S-������(.J���l��g����g�m�_*���������7b���vCGW��������v{�'�l����Py�frd�{w6�����{G��\�^H�/��d�d�og���]��z$��f��.��o{�=����{��]}�bJ=������lw���ly��fKw$���l��7tL��f�=�l��7cvn�73���3����f�������u���s����p.X��w������L�{���������
,
��j�X�����J�ks�T@��x=m����zh7��P������W�����Y�[f�k1�ezs�������i���{�l�f��>�r�;��g��r��U���������{9/g�����N-�o=�{7�G�����>���z���y{-�=�'��h��{�����,��=�}�{������>�n���C�u������g���6���/�g����o{v�v�/iy;ov�s:��+wv������=�z���t����w]��Jv�I���tc��{m����m�v����T�S�0���IQ��P��E?�4��{F��)�0��R3�w����������i�����r0r��d�g����������,����H�"Q@�H��H.;�.�� #dE�( �\b�/�����*���3�4���A��6��f+lNQ�b���b�Oq��n�M�w=�5�G)+��XxZ@cQ���H�N���"�lv������f��oO0���Tu,���+���H�� ��������O�x-a-Vw�����8�t�{��e��Tq�n2����^���J=!Gw����s�v�ED�{r�G���ToO>Y��A��
]6�9OI�W�j�"+{�(D����Z��.2�&*;��2�J0��������0��A�A��'��~=iv��+�L�K��T^����y�/����$�!a�K,��xO]��_%6���3i�K�X�nD���s���xwJ�+
adq����u�:���S�b���a��� b��S�����E��[�k��M6�-��xJ���{���s\�\F%�XOhRr=+]�nl��!v�*f�]'6��M��kh�2/������4QO�	NY��{����K�h��������]O	q��s�����������y�(L�@��i���>�C)���?��A�H��A���WS)}Ec���Ee��U�l���.>��E
�n�l��{u�1tt��z��J���e`iM�x�v1���C(Z��&�������������
d�2�U*�Yc��FU��$X�I
<p�
�����W��	"�C6�u���;g�lW����i�+ Q��xf�(pj4�B4�����c��y���3/f��*4�0�Z������y������M��T���������������N�U���S�Oj��W�����u?e��A��v�I���&.+��O�v�x��S�Yn7��;4H����Zfk�����z����]t����**H7WB���V���p�Z�	�5t�P*��S.<���Z�
4m�]=��z+wtq����9��O@R'W��\��A��ha�L; �� �KI��bol���wzo�f(���iaJ��g\�W�ujW!�����f����G2�En�����-U�� ��%4D=��;���V��[��.��1l���{��B��]6��t�7uX5�nL��7����N�z�6���"f���v�r��������Y���=]7A���D�k�����9����]r��7��c��XK�R�^^2�WG3��=;�#a���QGC���-�7��&�Noh+,���/y5��b��.er#.�����RT��=��Q�:Q��~��|_��[5�Kv���p�;�o��E�|<#.�����p��HO5�����E�v�����|��e��:�R���-�4���V���KS����I������nZs����cL�a<v����;{���)��)@������s��u�6m�m�%��y�s��-�w'����Uy2lG�/����Q��7h�!���w2�p��D��ng�����M��'�{@uo�Ol�c-�.���O�v\�z��W�j���ep�V�O%3lb�#��l6��-�mgkN�K~�9Z�F��=��;�>���������-����f���E�������<-:����g��m�r���J���vP�5�U��{ �i6���M��wwWV��i�������+�����W�}m-�o����w�T���[��n����,�N���nTG���vV����R�\�Vnh����}{�]2������8Y�oc�����5d�x.x����k�K�8�W�o����m ��qn���n�}{�n�s�Em�E�\��2��$�*]0��k���Z�t������`d�IK��u�8 ��B�>84�LS�U���9h.��zq�1^N*��H�:��ar&����J�|��r�q�\�KF��n�(c%4�������s�����@�G�M��}N ���P����>��<�Z{px8��Qwf�az��Y��4�{3�����
��m��a:|/��74}�Ye������,��w1\3�mz���l��5x����$�[��RY�:y�x��+����,-������/�eG�(��K/�+s�5�<�����a�������i����+��Q7���dyL_o!B`2�w����H������m0�3'�ft���e6�����
���Z�9�
�(]����-���t��w^���
�9��K�.7��~xk����b��e���9�1H�}ZGP	�_7�%���D���o�a��������S�S{5����+XAY�(ieS����#Cx���R����@EmI��i+c�:�K='���9�z��/����V#Z\���6<��u2i7o!Z�np�RR�W�^@g�t���o�p��1�*���Ju�L2�z��e���������{q���N��)I�c{����w���S���pJ�;����D^����H��U�]�o���EzCp]��-#wQ+mZ�x�f����k���v��.�/\/����V��`"c&�<5�]�xRm\����\��L6��]���B����}�I���Q;����M
�1��+8i����W�D���:Q��u�V�(�&��,R!Wn��+�`X�x��\1��v�M�'�|h���\�.��E�B��B~��-b�������E�XH�/���	�3c�q����,;�����\{���nPH�\l����������o�V��w����[nw�>��a�	�xu$�3#1^A�wDE-F�[��^lN&w�-sE%
8��4��+�5�w��`c�"�{��zn=����U�V��(\���x:p�,����y����a�x��9�������
gu�@2P��B���Gc;)�����nt�e��g��llw��HgC���c��&3T�7g�n�+*���"u�`P�OL�c)!�w%R�����9��=G�Xj:�2����4vD��D�`��S�HV�o���soS�<d]�#��x�5�s�(e����5�V�B�
:+���*��ks.a3��2��L�,�`;����93���-
����.4to'u�!�S�]�|��7�eJ�Z�X2�=�LOtV
�L�����>�@���!��9f����l��Nm���*�~�����9Cz[��+|oG3!��`��J�i���]���v������<����Oy�L�]��s�g:���?a��{�`;��������'������meowvR�o���e��.Pw�<3r��g�-�%�+�t:��7�6H��s��=�M�^���`pLg/{��w��l�rc� �����84.5��L3��wrb!cA��>;q��V��X��_lJU�B��%����W����)b��g3���Y3�QF�OK�qX��������/�f����.���H�=��^iS��g�i0�1y�p�tL�$�I�����|i;�{����Y�K����O��^"�<��*�A�Y���-�����aD�\}tuy����v<[��m��m�<r��x��Er���/nK���R�sj��*�����]
yH�cf)��3��m�*��h�q���7a�Ny��<�����|�A���lVYG�6�G����mV�
��p���V�
�
���O��������0��E��k�7Ti;�y[w����V�cB7��X����GCPvk-���t;7�*�^|&���j-WK���t_�	����]��=���5y�r-;��}��
�n}��{}������^ev�;p��!���.���M�j���q���`�����91��Lwv[�-�R���(����������\��.
Y�y�=:�+]�7�^cY����I� ��\���$��o��Q+��]���2q�M^���HUN����)�tnWG�WVS��!��c������������0�cVR$������gv��$KguK����Y�����+��W!]j�to�7.1=�_b ��������=�?j<����
��#�=�����89�5��;|���1���T;�/����70��I�iNm��h4�%��V�o=�:Zv\���V��{!"��n�����lv�T1R����y��y8�X�Y��l��I��>&p��\|��,�f7����r�X�La��g���|km�~U�[j�����kgg^��.�'q�Sm,����o%Ny�Ck�������[+(w^Yk}�����
��Gcj&�*��>:C����2�HdD-'��w��=�}���`���������Q��	���.���G��QSXj�3����LVL��O�{4�N����L��^�]V6\}���P�.���/���}�hC����x��/4lR����'{_�����[���1kJ�����^�^M����"��,���U���j�W�r�\�ie��6'��*9%Q�|E��ocqpw�@���J�W;OK��>^��]�>w�����Q�!������x��n�g�Ki�i_t���������&��(K�f�yz�:Vw�&�����W�U�Gv�`����!�������L����vo�K��[�\Gw(���� ��+��-'�s�d5�~��V�X�	����y1K.�g�j���V�T��x=��0X����Ox�p��8#�<5W%c�2��b�f�m�f��Y����kZp��J{[.y��=q���z��'8�^{�}j�t����Q3����<�7W���M���D���%n��^LX��u|�L�8���X�^��
����Zo��3/Ae�=0��{����']���{�4	P:�i.��� =a�*5������|���;���c���@ob#s#/��a�ci(; ['4hL^��u�f����<����|{n��~�����W������$���'���=lH�.��_
<�j<E�L��yK������ps_1�.�����3��L��<�H�Xz3F��B�b7]Y�tH�B��������=�u�[FLC|l��S`{Z���Y�����L����=��k�z1y����[���DX���rm����lPPr1�Nv��V�y�2����M��u����	Y��Z������������j�V3:p��m��=��V��Zb�]�j����V������]�z)#�P���\�Dk��3���J���[-3�>�%�/�����7�Ax����&{�0��������q!��(id�|�bv~$�����9�D�e�/ B�=����_���o��6��n�W��u�=CcG2��GF]��eW	t�c��os��|:l�i�&LZ���{ �v�W� �]�i���2�l&��N�dKZh[:�[�U��L����MX�}v�U���)mp��M^�6����f�w<f��P�����n���O��j������U���f:���XY�I�D��g��|
��W������@s�5��r����+��5��H���tX>51_82�5�����3�`����u5������/=�7���o~��\N]�}yX����c'���S����z;����d�xL�L�`�=y�PeT�C���u�ub������
��N�r�	]��GUh	
�E�	����y{�a��.�,�~s��=�Y��k���=�`���kCm�p����N��?>��Y��0l��n�R�bsL��O���� :����}D�W�:�������`g-Y���d���]��P~���i��J�o���WH�}}Ejj�yS�H��Q���}�3aR���}���F�Z�c�����H^{hM�N��i�vE	�dCV"�.a(����$����2
�}�s/7c�f�����:�v�<�p���n���>*�������W�b�~�#����s[���@����b���J�
�L�����gT�����p:6��`��;�����|W>��{���V2���wJe��O���e�O��z(����:'1�G�BZ���:�6��a���t{�����0{�X�N����"���&%	���_]���Tj�Nz�8���8x���j2Z�2���(���������Vg>S{]8�S &���M�0��4��;���������V�.�*�lU'����������S��tx�>�����3�TI*�g>�I�`BY�<��d��Uf�y|e�c�W�[~�?m�h2��G�������g#�����z��d����9�_V!�m�oh��z-���gqk��w�������v[y�R��
�������`d<������y���_�n�����D�z���������FOi�������*(�*�^6|y�{��e�gxw1���.cO�����������
	e�pcUO+��mD�����[M�<L�4�j3����z���������al�!B��o�"�Dn�r�c}n�o�.������x�2��]�k;�0���|��`Kw���GF��!��l�u�|������{/N��v�����c`����{�� �_!����	]�0�w7�� �<��b6S�ol������k$dt��loX{�2�;�)�
M��.q����dy��Y|�����Sv"�������
�����
���=������\�
�U�s�qh���S5�L��A2	��Bhw�4��V2��'�^���������2M�C�~�{����#�L�5�Z�H�w��`�����ga*�^�>�<8���
*o:AF[W�-���h|L+w�+��5=�.�X]R����������.�/
\�}��?4xe{��������V�K�ww\�m��`��x��H�R���Z��*k���Z�b�J��z�����9�/��7�u:1K�	���j~�1���N'W�u��Kq�������;Z�c���tG��+��3�;�]	po���X ���x�/+=�dE��d=L[��-j�����+���Dln��9��/��'W(�9�=��,��XX�aX���������7z��iN�w/z�QT������6��L����g
n���s��:�.�����`b��zF�{��<��Zb����'k.�>
��wU���(^L}j�~Y��N�'F�Y�J8�S�+,8/������Lo�E9���C�1������9����)X�7S�N3���
v+Ap}w�|U�t|��N��U��d%<J��'�i�B��Ew��U�y�����v�������3��ZH����v��R��s�]o����������wY[�t�*��M�Q:=,}�b�]�����=��?t^tm0�c��IB.���e��:���[�3W��A��r�MS��������s������sLe��/'$����*�N�K�}����}���d2$���rsgn���^H�u�zL�+>m���h�O�w��z~��Ev�U1H�~��r�`r���%`�/�����;k���hNv���)��9�u�����Jx��vk��a
�]�g����O�b���/�H'���#�hv��e��Oe����E0�.�k}}�����i}M�Q��r����pf5z��|��v���|��]�{9����b=��-#4@���V�3O�_y�Z�'�a,�u�k��N�rk�Jv���9�<�O���/�:��V�GU��j��;�=>�mb��~���m��T�]�Y���}�[�g%(Q������"0�Sk�/&	����f�&��a��me���B�Yx.�]����2��O���d���E��Nxy����������G�}�����[
`�|�V���;3Z�������6Mv������z�-x�k'�=���c������k%'����%�z�l������V�AB�T?`�B����������m��W6�6c�X�����������!u������/�c����*��6���jo��s��r�n��~��R��
$�)���(����R^V��������b��0�Jf��zV���*��\��6j�9�|^�'���.������q�����Au\K�eU
���M���4M6&p��R��)0�2��leU.��(+�,8�y�K���(rj���g����' ���cc�z(u��Qo7�^_:j^&_S����7��������|~
4�H���Z4A���Tpv��F�.������*�N\�w��_K3t���K�>�[�N���}��Yz�>������^Kg>8��2'7m;��^�Ew��\�W�{��tO���B��wy�����M����z����&9
H��B��}�U����k��n��X��5���z��b�:����T�����:��>�	�H�������/{�	M��	K��q����49*o&L��v]�c��7JVun������n�a�x������4%����Wv�8F
���]t�����P���)����k���af�[�l��z�Lj�z�w�=Q�<������f���*V}�x���#/h4T�O{��N�"�����G��^w��nW�0�N����\�+9����M����� �&���|�'�+[W���l������=Mb�9��$R
��*��D]����! ��S��M��{�u_�c�0���_��p��@��o�|u�S��b�3��2	`����:�^������JE������_��]��k�O	���\-p�M6Zn]tj��}u*�Ma�
��Zq���#'��#����k0]R�C_bz;�k����
���y����$�v��_��5�TPI�g�(������v������>�4��]�C5��e��-���U�+��;�M�[���V�-��|]�y��L�$OYu<��.
n����Dw^�O4{=���z��v����~9�H��;zD�g y1���Vw�{��Cj����[�s.a6�N�u7k�+�wW�R�DF�s�����%L!������������0���������74��/���A~saRV���T�����4�}t�������{27G���4A�Eb�][��3=���[���0�e5�)h��9-_USQ������!����1���0�B�jo��M9���*�{h�&e�����>�RB����\)�T��fwS����{����\������{�W_�<:�X������]p���]L�	��]���P��z��g9��8D:�G�s�P���gj�lP��i���=����j�b){��B������j�������)cI����7���M����5�z>�H���e��K�L�A���9���9��>�y��
��B�<�+�Dw�{���>��}��lK�8�'.�x���@�m����(*6� a`�����#	'jp�z��1W����M���h��\��cX{t9�6�
�����'���l5�k4z����P@b��l=��N���X�����RG�z��D;a���P����v�
��:t���SGE������������u�XAo3�_UN9�/4z�WP8��ynI�Gu�����vn�!!��{�{�blze�S��*�GI�[���Q�Cx�wJ^��9V�2�C��P(u��*_	��6��3�����0�������N}C�KFD��)���x�~��b�(�����1ypx,�X���v��(v�������n!��X���w�]��4�.s�7Zu�qLJ�D7�m������
����V�N.���8ToL�cwkc[3$)Z��V����[�f^Do5�l�li�5�.�[��3l6����[����/:k/��j�g��'��+�x
�GO]�1��Y�����������7a�����sb���y�fU��%a�;�y�t�����1�u�	����z���$�](�{9��`S�:��2�����X~yXXC���|���k��Y�U���E������U<[6�zO�MZ��	Q����*���J�$po��q�y�.w)���C]Q7{gid�j��.w%�,v�|q��j�o4�
9,�A��7	���q�w�����c�Vkc����M��:D%�]�u�r��P�,[S�)�[�-�6��U�.���6��s��~���;�!�C�!G����
���U�c�:txeB���Yw���`F{��{�����Q����cV�^�T�r�m-7���%��$H&*���}�f����o�9�9��������(�}�������sG"��.��"�x��;g��=�����.�:/���d�5�N�ob�,jWN�o!7Fw5��������Z+U�U�Z�)c]�m����{��@�����hU{[��S�X8:�e������&���@,�)H���%�j6)GWu���U�^P� �l1��R�q�V�qaw&K������xoQa����4����I	��s<y��5�[�;�}�h[����y�V
���7����J'."��nc���T��_XZ�3���H@��/3���k�%����Jf#0&��;���/7��oV�������=U�������3v���L����ps�h���KF��lv����g~���JWd����<�i]�2�;��F�����X�*�w�S���6��qA�~$��c4��Z6;�%\83���$�T���9u�����.g���,�`YyW(���� ��-v��g=��o���hmE2�A��d�cs�G�*�16��0��"o-�z�����b0R2��(.%f�{����4\�O�)��M-�/6��7��w�x���L�)�3 ��c�s��VK�vU;!����<�"��T��s���J�	��j�x�;�pn�3=��$�<��{��CMU�eh�Jk�_
�Kz
<����q��%�C7J�������!��'���n���s���ci�����T:���t�I�hU��ogq��IH��aa���W���>Y]�K.>�[�����B��Q�W�ht���x[�m��@8����S<�6F���nzN/�Z[z���;�	�4�V2�����\c��P��L3�>3��f����vl��;���,]Z2���������
����s����N�XJ^����0e!
1.x�+�R$�m����4����^VX<�?b��}N�W���p:��,��9��>y�_9��AjD�[�`as*�O�&iz\-y���p��\�.�5r��z��f�^p��b�u��b�����e��Mss;���Z��`���x�8:s;Gg��e,J�{��+�pW����
��)������Wwg��k������%]^>��5������]	K(5���9��LeH�mwO�v\����U�Ma�
��#i3����T�V��0��x�n������N]���j=x��W�R����7U���{���=�$��{)Vy�A�������]��3j:����%2�c0*�"
�����6��_���[=��Z��;���&_,�2��;^��vA-z���BwR�k#b��;�����\���7�����Uun�)�
�n���jo5/bg;W��G��<7bf{hN]�=����q��'k-gnT�6�W6�]�A�^�L�}�T�����t`&�0�E��l���ODs��#L�x���P�F�����`�Gx��m��;�*T�$��S1%���wvd.�b�y���e8����=����.spx{����V��{�	��>��x��9�I���d�Vr(����7�xs!{hb�Z�z}~�@��P���5i�m
��L���Y^~^ZW�I-Z�U���#����vSs:Z�ywt����������NQP�2�����+9-k�b��y�HV:��hE��Q�����/v�^����x7u<{��9��M�)w-���`q�t��[�F��}v���M����
R�q���s���i����*aK�<����+4?P�Z�E�:$�%��b��=��/�<ms;���7����;/q�;
���
v-a4��{EF���)5�e���4�y��R��.ZvM_���%�N��c��u��u���x��M�^��{�|L��R��jG�����+�1��������3�u��B�'u!�Fh�E[pU����N������#��j�$�^��������������P�W�i��>����c���C���r��{��Q��q�(��)vp�����v�����k�������`DW�;���+����k����]��
�o��
}�p�Tf$����a�S.��#-T�������"�����,�#b�Va�����7'�b)�x%�`�"�z"�Lj�n/��+�fL�T9���Q
w����z�����x��Kx�%����+�&�_'�+)���u�[��_���a) W{e�����%y36F�I^���#��)��������8�
!L���H��2�2a���</:���C���
�i=rt�a�4��������WF�����K�����'1�;t(�[M=��1>������=��m]������_��x3���wt������x
1X���m�u�M�g�&��Gr3���.b��>�<� \������{��gmb{����o=�,��yR	�|��:���k2�6	��Zb��EHF*:��]�@L���U�A,���7�Ew�9����_���4�$^y�P�.��.����5cD��]X������z��&5
����\���:��fC���p�'����c�=��9������/9n�fu�9O��i�va���J��E����g�EJ���������i��bKt7CN��U�������������.������+\��x�|�������{1��w��in��X,#g���/�T���]^���
V8-�#�]�V�����X��#^���@������/u�=�c[Y9�%T�g\,�*��`����7��XZ�q-���6�Y(��x�]��A���w
���e@�U���M��T�wx��j�
���p��un~{#�u�`�
�����<Q58����}��"��^��
��!�]�t�������1s+��e�5�
 x���������s�S��z-�����Y�:a�!Cc^���gc =���r��t��}�,oc����Z���1=3�����-[5s[�bs�/X4����������&��/:+$�,�9�0�������u|��&sKiK'o��TS0r#��r�]�"�v�8���������Wxx�q��)���q���B��)OC�����h�)wr���S}��z��M���)E�
��u���/�j�eN��]��+an���E�?iCx���lw*��V*[�V\�`[���=jc��c����U��oUq �\6���v�LY�,xsS�;F��������ay�S�0��w�R�����]{56�\[����d0�;MzJ��=�jc����|e[B���%ucV�=^�	��lmm4n�N��5�-�sh?g:w��CE;4n�L�VWXz���u�g?Y�Zs��=k����������z2�m���T��^�����GV��6���em��vz�ktZ"�f�x��hC�*��K���U[C�jB$������#t��6�"4Pm�j��+���+(yp��6�R%_\�S0^��hZh���o5��W@����8{@�X�T���(k���=}�4R�9f�GI�N�����-�3/�M�L�72�U�+��,��e(/yh���Ca�.k�9z��N����Dk�mz�|r��g�������!���XM1w��}��@�<�c	����t4%����=Im�n��O]��T�]�Y
�>D���y�K�J5��Y�#�h�DoW��Yt�q���vS�V���5v�I�yx�;k�����������pv;�h���z���$�8\}.���4*��j��vm9S�y�rq��U�X�#5�F��c��W�:�<�*zRdV/9w���R-���O�<�zaM������0Yw��g��#s��o����)�+6��S���d�3����>�W�j�^U�%8�5f��)��i�cR��[�:����%�Z(��E�`��?>h-sn�=��{��)gy���n����c��B�u��f���z�xd�\E>��}�r��o�n�����#�4=�+�'����9q�Fr}��]UT���C�p����mf2�/w���[�1��U�!r�
J���1vc��O����pf
��)��g<���
k0�E�o�[w���j�w�v���b�5�d3`F�m���pX��+Z���aA�O�*��������\1��8K��1Uwf��u=����|a��8}}</S-��}�k������z��7�����#+����	,v�]<��tWaf���iJ"�-�mtQs�\v�Y
H���|o���U���n����Q���,90�sR�]��d��6�'dl���6J}��/@���P�����pT���F�}������V���5�����8���y��l��a���w�I��!_l(��D���>��R��k~������[�i���r���6�S�5{EPKh!��Bs,q� ���j��1��k���*S�T�?&�*#t��}��^^��|T�������2������	��
��1i��
�(vp�7�l���*�����{�U���w�=2M���-j�w����:[��Z�������O�0-��b����:,����^vP>��������]D+}Dk����G���!����t�XYw��j����H��Kw��B�a����;s�Is^��f��-���_
T� =�R�j&��=m����v�����p��vD�����v�_z�l�7��t�=D�t���B��l<��/.�b�d3�(,��
m#��x�8��pevEWyz<��^`�|���
�[p&�`]���;,b�f�<�^&��	w���;��R
���K�Q��:ng�x��N/}����?�V���U�n�r��o{�uE�5!��Xm�w��[�eYJ���n��r�=���S���k��r�F���&x��]N�)���|�����jI�b��U�t���u�|���������R��w,��+LB�]:�}�U�?G|�4u�B$���-��K�D�����j�a�C��j��.`�P��b��8�i�uK��X��ny���!����?5�.7�5=z��y�1�Z��L_��u

�`�&�j"�=�����U@��o�3���:~�=�`����&��.�N��l\�����J�����l��{\����c�u�c���'m�;����t�������XF�^��x!�|�nj�,bF�T�t�b-@G��q�vslVZ\]��@��:o��4���tW��N���R�]i��w]�����vd��������.�s��nh��nn���h�(C�&���.��A���[N�`��{3jw��Q�f�x��c�n�K,J���o��x8�^�(*�'��f���l��Nv,���<-��*y��Obt�;/<9��������l�dt\yn#4�o�s��	YV�`��j���n#�R�uec���K!o����<>A��}��]&��8i4D��
�U	Y{��V�n��U�x��.�hC ��:�oSI#�-��x�v��
�bm9��E��q�M��Y�P�=q�}�K�`g��G�yl�������<,5[�QY9S!�B���g�t��y���]�j�wQvj�1��-[�Kj��OyFWBi��WeY���f�M��:�R��
��&s��VU��{8`���x����>/B�u4:���dzJ7~G[YX����s:CzCz���q�����v�{�/����}�:�Dt]vD�u��p�jX�v����t/.b��a���B%��Ow�Ap7j�N{6��5=�X�3C�������3pnu>d�����K���4�U�Cp��wV%$����Y���>��n�k�D��$<�n�������v�S=S43<�����]������&���\j ���-LX��1M<��Y������5�v�W`����hE�,�SZ8Nv)��?x���1�
������yubr�����F���n��������sh��sJ!
U�u�2��=v�m���2X��]�i4S��[�q����o]IGhU������
v���)i����VT��[����~�^��Q�nLl�r�z����q��k���]�{Dg�|���3`���,!�-.����}gt�x�T��co�|<�^1�|�t|��1��+i�/*{����z�8-I�����V�Y�n��e�lQ��_��t�����So����xN]
�PA�����f�j�H�\�4,HH��^��\��6��S���{V���p�yvX�d|�����P���6���;�=�+��N���k�$�!S�OKPDd�\=e���X�����~^�W�g�lR���b�r}d����/)�����b\�;�G[�b��sV6�OX��(��T4�v�����Y2��0��U	������^�@Eh������C��J >p���+�l=
�^�I������J������in��������~�!�1�O��xq��
b�����Up�+��4��d�t�/FI�������u��L�^����@8��	;�t�o�"�(
^��
�X���bd=<&�q�[Y�U�%h����S�6$��h���yb������g��-�m�4�j{�?!�R�����Z�lq'���P�x�2Gt�3{T)����0V?��7r��k�b�++�tP��\cy���`��i`���"����v&�V��\R��{4�����Hmh�t�K�w\�l��oowz�7��5/q�9>G�W
b�u��>�^����{�z��S.��~���b��"9�����H�ZS����l�h��^����#���y��E��j7�?k��� z5���Z���v�j��s-���B�;Dm(�����+�Ku��G>D�������Q�c�[��q.����7��DJ^�r�<����x�����h��Aco<�_k< �=|��m������
��&fy�;S��X�3��i����`�*{6E��lG71��|��<���uG*rP5M����G9%���A�CY��`��3���\'�����i�}t��4�Y��$�F�g[Y{��Vw���;��������`,��B���u��������:e�������ie7fx�x�|�{��k�n�\�ednFR��[.])�-UwE���
�����g}F���tU�^L�=���.{�CQ����j?"x��^c�8;:�Wz���n_J5��\/P�Au�������V-Q��k�Z-5��u[�N��+H�����Gp�Wu7�S�������n+�y�d������hU
o_0��/y-��K����R�:i��v� ��qK�����j����5��t���V�YU�yt��������k�����y����9|�'��A-��a�����MxDF;��u7�-�u��n
�]�d��`5�m	�:���k-�]���^q�����O���-_�����V����'reWg$�M��l����[��+T��)��Q�Yv��.h.�g�l)4;���0��T��Q�s4{(|<k��n����J�M�n�>/&k�4�B��.��6���������w�C�5
�	:+U�+���f��:<����U��U�u��gN��JS��O1�852�+]�5���M�{0_J��vU�"��vn����2s�M1����S��
r��`��(c�1�<�1O���A��5���u�Kz�d���I=5t�J��p��D��vr\�������k0���;X!	�������m
����{����{�K�s������� 3�n��RXY����7��M�A���H����{��]��F&��������p��}�Z���7�v���]�9��:?���-��m����]��f�o���m�����������YY�8l����	B��5L���0��U
N��}��U�����*7o����IO�K��Z������}����n�(�\���%X]�Ku��:�v6'�7�i��������v:���s�=����}�����������1�$Z5�[%���_>7\���t��pp���������7Ee�	��j����U&E�m7�<�[�
fy?9�������[Mw���Wg��=������2�\Q��$������?Qx}p,����4/0�����X�_m����O�]�DD��2��93Fp��+t���EZ"�Ri�,���w���� ���e�^�/��f����S�r���Q�-Z���p�l�
�P]�Y$txe1��T,�f������570�����E]OG�R�r�]���f2��Who��uQ��C�Y��Es��ouNV�4�{���V��@��X�
���T�d�V�~�����w�F��l@����vF�����9�F����<)yI�����z�#(�����w|��!���auX�x��I��s:���y����CX�E�����i�o���gC,���sp!kL�b��O��1#�U�U��w&��Xwz�=sv������EH��7���g����wGN���g�����������TW��%�o���*��k;n@��{-u\��H���n�+��h���X���m.^�}�������o&#{;����wT������8c\^xK0J��������9g���Yh%����y��i;�;���y5��=�<�w7nN�8&�`]�5���������Wp�]^;�Z��{0��W�����,��KJ|U.���krk�����oiB�5�����j����i�/����gK��� �u���H��`������+5����p	A����1<5h7$#�YK=�����`������-��X"h�����M��O,mg�n�:���K��=�r!�s_�M�b���F���J���]���2V:�X�����v��Ho-��M��J�3�6�S}��(<Ok�^��u+S����}Mk�����*;A�X�}��|.6c.���!�$��%��l���	��V"�rKGJ�--pT�2$a����E7*z��i�����i%�sX+��p�9��8+��>��T�}��M@e���������Q��o�H��k�e�,kf�9jezd���a�����PY�*;k��
x���LY�i�c����n8��NJ�����<M��]��7R�8v�����z�+��y��U��K�����h�U1j)]6���6�.����kV�ox�����5��^f1[��Ov�l1�T��������{W��7*�,���g��k���X���m���%w&
-��v���\�������{jz�t�����$�:Ny'+&]W�62��E����~i�WU�����:���F���cJ�3Kk(Z��+�Z����s��c���,��y������/R�+�V�����W��{B	�T�Vu"�>��q������~����Eh���w��S}�_qV9 r��u�������tQ�������Cr�[�^w;r��n`�T>�8xu��xi������/��xi@���C��C��/���z_p�1�s���s+�
��K7R����mg=F�u����h��������=���������Y�'�a��|�Tr�s���Q�:�{�a�K�������snd�J��&����=���
����d���a����GJYw1�7��;eoMz��V�B3���'W�������!�5f�1��"���Y]+O)/cx��*�t�J�y�����<�_v����*�p����h;S)(�sHY����������{u�:�!&�$��.�Nv��V�d���U�/7-������_�a�7�Pn�iYdv�KEX�����s)�h��rJ��������[cr0j�L��7�;��C�W��sV)A�6U���'4A�5/�kU�^���A�C�.]_�f�:�����{`siT2�G��+)Au���s� ����;'F���
����-�����������J�m������* y]��K�M��U�<�T�&U�5=��0�P�Z�kO�����3��W�@��Nm\jv������	�����o��H��u9V��w�q6#/�=���������:�s�d���6�z{y�� ��+b�y_V��e1�O�:���~.��bW�XN9~��Y�
���Mw�~�~�:�^���-<�V#RZ�/Q�����������8�%"c��Z���]y�b�U���9����N�Y6����d��{ss�Pf{�4s0gX���f6^��bv��od�(�{��b��5��i�s��xI�~����6�H
��-fm��HW\�71>��C:h��1"�&z��z��Q�W3�.�eg1p�&��:��n��<y��)�����3Q�<�]1��d�M%���<�r�'k(I��f�����������������,���qUY��pU`�h�
��un�S�����G�~9=Y�O�m���t�W��X��+p��zk��b�<D;"�q����D�\R���=PP������/P��{|k��^<���Oj��c����.K�{]���$�v��,�i�`qp�':�����h�Z$Ak���9����Z6;[���i���K.���Jt����"O�2)\A��#I�^x�u����m���%b��L?u��B�=MZ��;�;�b�a�1')�yOM�D�4uy���P��sr+&|������H�^g0ub�-������D�������>��0N�/X�z�'We{�Y9)y��`���FW^�6��/�]�'�.���1[~%0�m�=�q��Xl.�������(oZ$���,��}��@n�������#���h#�Z��O����v��7#u�o����W��D+S����"@Y�<�����+�V3�w&gxb�p�+p����F���m7��^�<�r�zH��N�;��\t� ~j���.���v�x�!k(B{YPS�[�%8�[�%��ei��N�b�/-���c��%(���
z����{&f�l'e��Y��K��UO_TJj��D2��w-ui������X6��W�/&c��Q���:��v0��^����6[�1��WZ����wVvD�%1��K�����+�g�5�������������4VC�f�}�;�WW���X�0�/Mkb'p�SMo�h�'����lL���=Me_����f��c�\u\�yS*��t7������W���Q[�`��,(�1���C]|`�������g+�5�����H��������-�}���7Rm�JU��l�xE���
%l��F�`��j9��NP����2��Sa����i�D��E��z���m"�=����ye;;�7��	n��@5�dl����F�b3s*�;F1�|���5$*���.��������]�������B��[���r�s�E�<�}�0���,�M���h��r@������H�;��1�}�������
��C�[n�
�t�nu��5�Ov�8&1�����[�\x��Nw<3��S|Idz����
��^�RG��OF5I����$���F��"�ru�r:buk���/SOPKa�����PD���(d��))�c~H���������o��8v�A*^3B������?Q}���<&]{�|��_���?/C�^�S!�Yt��������;�@��l0����>p������s.s�C)���rx{��Oi�CY�H	ynxgZ|�M����m�>�u�9Jjt�����HKc�����L�u=�+z�S�B��(^{���ft
@u����x`Z<�LR��osn�1�{���L����N���}l��s������!f=����C��WqD�A^��O�a����������U����*���n�����]	OT��Z�����T�VTyN`�^�|�0]4�C#*�u<�5�[����;�7
�]%nSa�����v��*���6���k&J���"){R��o�PAfOn���5���g.�No�G��l�^a���?����qYi��c�i�k�5�]+����-0l��^Z��@�H�����d6[��7-�d7T�.��z$�g��<��<��;���~��{�s����k�:��
9��6:>'����R�^��O����IT��2<5�{��+%�Y<�wT�������>��!��g_o���GT�U�����c�@������o���-6��������y���.��+�wh�e^������F��4Ya��h���{=�N^��X���-D�`�<�����vm`i��2�W>!�sKzU�V0f��tp�Mi7��H�R�F�\������Ssf�"Z���v
��R��R��m�����n��Q�z^�(0�]%�1�K��$:�Y��Y�R�z7�������w�i�o���L��u�w�
^���b���V7�7�*�Owr^���=�[���>=�A�����e�J�7@��_WT2����q�1�E�=dv��]rfV����=C���n�X��.M5n�AS+�<�T��k�����y��P�x������v/�9��/6�������������@�z�H9��4y|X���p
'$������� �lT��l1�V�S�cr�06��<�v�MHKmQ�X��iH�I����
g��r� {n�<��{�����ytDy�W7jsE��������\�G�TT����c ���7�������Y�T��:F������8&
g�sN�CF�RQT�Q<b8����U�O5g����i�Dk���l4��H)S�.����hv����I�(e���Y�l'O+���ik{���k���h��L�=sY,���mv��s;WV�n7\_���v�&��WucI�P��b����OWS�{�=���1VML��@..���!V_=�9�2��[�qP�	��Z��w�fd���$����Q�N�hOl�����m�Ij�9\��tz�+����KL��4/%pUJV�7��^�}o+��F��o~Kz���}���7k�]jA��R(�60W�M�/r$@���`��<|������J�)��5L2�py�}.���Y���i�:��m5�^�������7}��w��W�7�k���,�7������#�5��yO�������3W��p2B�q�x����oE7}�0a��t�]��5^E��Ol�������M���0�;Az�R�^�!�o�t,u�S��oO��vJuv��?y��+=s:g�����C���#��m����}V�j�����Q�����YrE�Cf������E�D��@�:u�$S�$�H��+����p�,\}x������GQ4#H^�4��$��qM�K���Z�0f�
�����R���{�e��b�'�XOmF�SIe�����
�]i����c�>G�qC������Zy�����}K)2���v(=��]Z�$�b�6�n������72�M��-Y���Y[������W��'$-��.��*��|���SR������,
��b�U�>m-��G�����;��=�o"�7��C1sN����il���^���r�PS<=8�r���{��s�eh]
}p�[Z���p[e;��}��^�R\���)2��w<�n����}o%���v����o��S�1z�����<�;�3=���]�S	��h�s�t���.����1��vyi}�XPHO�c�}�XTc�34)��GK��������'��V�>oW��ol�a~�����r�����M]Aj�������OR����3VL�������W*>����I����o���	�sf�X'<�D��X�0����p���I�I���2c�GJ����P�t�o,�>t���j���]4�g�����]�z3��V�b}���_�k�
��^�m�E
;��9�N���`�s^q����������������nL������On�w����G|��<|y��B��^:��<&��T5�6�~L�(,��N�B
m���D��h�4�����w��n?����r��Z����g���(��9Kq�V�����q���������/!K��[��M�B"g"5�1;��r����|������[L�oR��y�j�`2R(E*���\���><��B��<�+Od�=�/k�4k��^I.}������V���,��������(�j��?_{6ht��I9������-�'���B����El�zT�����r��V�������wy���K�e���O�xEY/�5~�&�>��.']j�t�C�&���L:"�
�#��5�y\+��.9��N��_*�x�R�x���i5����+�O����k���j�����5M�����\��7$���=s�}O��}�f������6���iq��Kwv��_7��+�FE�W�I��%�����a<�2k�T��������������o�V�$\�|
|�*xB���nR���-�K��sv���!��v�k�f���)tVS��B��)��|#8��q�]�����xJ����04�F���J�{�8v��Iw�_���bW��%�~ol�"���V��w���e�����x4�.�j���`�����F>����=��m<���H���r3bz�����%s`�5N����@<�h�@��4�"W(Yp�������a4�}�=�o�.���Et
�U���1���Ur�����^���5W}�,�,q�,��z������9�J�D���T���b7
e��0Y�9�	���MA�}D>1u��Xm�9�U�;��S:���{��{������AW��������,1�.;�t����/9�43���a��Ip��:�����^�9���}8P��%xZ�Dx��Oj���\tQU/���������NaK�bS�d��SVm��4����O%�aq]1�������j������HE�Oy�]NBc���n�u�p��:���'���a�����`�������"�i�{�+Y~�GL��?h�mN�1����_5����������s<{�E�(���.�[	��[^��*E,~�~\5��S���swCs����5Ni�|y��j��O&����d�I�|���M]c�Y�|��R���[�Ko�I`�K���oB�BMF6�h���d���C�2�����3s�a�:��"����zh�V�
��\�1������8W!2����(���X�}t�]�;���A�.s���F������IU�����Z����Jy�yk,��1��\NTip�u����b���'�k�,����;W �d����z�m=R�#�l��mG�E����p]�h��{���)��5L&�I�z��8��%m�Wg��������;���(+,a%-��������d������r~KPVP�:�o�[R����;�^u����e&_��Y�/^R�;�������x��@�372�r�k2��=��9�`"o��G`^xW��{E���d�T;����h���B���c��G��~����9���&7Dy�u�nn
���Cf�y�1��k�:��
��Q�'��cdgT��+��'rshgd�zq���*�\B�TG%+���[������v�q��~�
K���9��w�����l��&{�*��^��vE}X�+^WP���(���4IlP|X�
�H��g�#����-��������bx^V#��/q>����n"��v��Ii�g����}�a��}�{�<V���	ki$Y
�OF�K�4�����d����t�[��\��{G,� �;���V��wU^f��;������{b}�Q���~����|������R�����/�&�(J�JNY�U���
��]H�	����a�bl�����^�����Ot��&R��pbAR�Mzo�gd��%y���v"��+q��d#p��N�e�M�!T�vcE:�0<��
����m�y�����!g5f���%b��Im��$�
��n�����Na%�]�Y�;�BP���*+@�8�Y����m��z���C��J�q�S���;����1N^�{�����[t���&z�4��n ���:����*
,��[�zAO��q�K1�1��7A���oZ��y���ReX��wK���FP���p��rTP@��P��^M��2�8����u�]o�{
���=K`��@fc*��k���e��x7}��<�Xt���7����!�n(�Om���q���#�}%&}g��Z;l��T���3o_)�V��Vu���e<������-�c8�m,��c�Z�/�p�Ck�"�7�v�`:(]�3-<u}�
v���W����&�����\�������c����U����Y�jj��������WtJ�xl�
V�/<�7��������������������+��T�����ZO������U���l���V�@�� n����p�,�,�Fgf����A�V*��'��5�Bu�����%v�l,�w�@R�{s���~��j�}����P	X6��s���O-+��f5����me����)F��]R-o;�FO@[������}��4���a:9�w+o�;�/A~=
��*�0N�&` �����oW���-���!P�V�����[��L�������q��������1� �_kX������Lj7�g0�1����<�d8�|����W���(������
��M��A��}G}M���pv����C��L�5��~�DB�[����}�p�v'77|�x[�D�Z$��`���,�v��������=����W��ew�lJ�~�v��W�*I��r��������j��2��m������
-�i]�.���;���d���|n0�~��t=cl�Jc4]j\�^e�>��p��o��n������b=����.���n���r=E���
�4|A,����Yv�������������x�\k��N��r�i����E��i�����j,�������}��>�qK��������{�^�s|f?y����3A����WZ�5���gs�Tt��"[B����#u�����A%����X��xfs�G�4s�
h�;j��z��	>�Or�1�E��y�����X�'+Nu��h]��O�r�XY��f���*������������2�^���PJh��zs���vm!iu���\�Fj�{'+�u	�7{�h�Y��9���`CTu�.+��tU?DU1z�]��u%E�u9�mx��+�6U��YCI����C��������t������1�t�������f�F���������W5�^�=557(gX
��/���l�v�OJ,��q0f��M~q
	��������J�,�����t���dV*���1��v���q�Z��s��e_w���|���fS�#��6��~��d��^�7 �)ndv��������)��o����n�&Oa�;���l��y���%X���\��e�������3�=g�������p-�SN��+d����t/~�����4��a;��xV�l��������_P���Q�k�ko��j����>�"���6D+��u��,N�\�8�P�M���^�`�� Q6P�]�Yp���,�0�����[h�b���w��_uN���W�_����E�	�&��x�9>�O�H6�=��*sE';+B��C��[���O�G	h���g�]�<TF ��@�p�4�����+Ob��`8��X�I�z<.��1�C���8�/\�q�D*u���}��,O�f�������
y��.�f.��i�0��b�}��5�f0�Y�gU�oh��i��f�1m���3?L�F/���vhr���Zy�Ny�2rZ���]��X����^6�-����c;"l]GRe)�����>=�W�|x��J�'Zd���TC�^�Y��r)MDC�QgZ�0�������,S�[��U	��	��u�(e7Gtp�.5�X�3��"��N���n0ZX��^�V`���+��j�!���-	j���\��L����j�6�*'����)u
��
�SA�m�$U�^JY�/=���Ss,H���4���E>�.C�t����;����s+��(���\�i����$����c�w�:��),�k'�A�4
������>�y�1nN���W�K��iL�n������%�h����)�:�V��M����]CT��24�i}*�~>��|NjN�>}S������z������y�^��EK���^^1���������E�fy�������=�D�OXp����;���������l��Y�p�v�+���K�B��_ol���Sm!���S���S���'����������
��~7��<�������9iB;5G+3�������C��H_
������tI6A�`�u���ZEm�����p�3X�������
�#����%=����w�^�3�C�I0�\s7����z��������S�}�A�	�������m�3�E��z;�|�|��h��\�l�������PY�s�OkUZA:5n���](����y{/%�L:�����$����u:�:���k47��f���>��
��E-(�t�]�=qbAm��a4�7�#�u����������}�?��n+jOX���+<�Z���9�������z���4<�����v������&�o0��Y��4h�P�f�"����<�d�����F#
7�&B��9���o4��sF��B�.3/��Z��
�S�k�jAm��LWy���u7vi�OT�y`��]/:�*��=�4F7�+u}�`����&p�����F�a{8q�j��=�\y:z�)������q����=���0U��`��3�`�xz��5���SR+��0�s�/����K����}a�c�u�l���EV��]_k��T��FS�����
���K�:��{�-5�Z�-T�{&`�35t��7p[��� �\]N�l���P�]%?=x����
�����EX��[��� �����fxf���)�f��s�;�C&r�2T>j�@��q��d��|�.��J�@/f�@���\�Qx����m���5�V�n�{)��-�#t��T�
;G�ua�����q��Q�5�1�A��^='�cbd4���}"�:�;�k�%�Z��I�4��vo:���o`�O\n7�vL;�8��J���[�q�����,�����b��b&E���W�_
�'��lp��l�.W��{c*g?8�F-9L�(����������ON���_,�GCV������3c��y��iu���W�.��V�v���g��d�w�SO^�hd`R��k9q���K��h&=�����c(f����*W~�>X��	���?z�=�Xps����o����g�������_!��|��^���P�=�A�/�u���|�:�</W�KW��J�x����-���� ��;�K[�|��K`�U��X��������P/TOc�)��8���"}�H7���3(�������utF��;�YLS��Am�"U���q�G�$�;a���i>gy�k�I�CRj����WX5���R�o$�C/ip}C{+z��h���(�S[����F)�/p*��N���IC�����jz�K�W�;�m���E=�lW*�[/�DV@�������#$����U]8��*�+,�V�o^�-��^a`U�Z)Fi���������r��t1������+�3y2�.����(��v���NtG�$����lN�.)v/�?�9Un�8��e���\�E���7~
���4�n����Z���x2�����x
�VL��V��-�{i��=
t��y���LwZ���[/�Y����<�V�]=��
v�D�
a�!�D@R���
e�e)M�P��4�K���/3�u�k���<���F����'�z���s��������������@8"�5�p�q�B������y��������s��xh��9����
�v�W<��k4U��Ksc�J1���JYX��kbp����wx\��.���R��������Z��G2��s���(�7fc��a���y��8�l0��1���v�/L�4q.k���q�#�|[�4{s����t��R��j�P7k���g�[7
@4�-PF���r�e-�_��6�.�e��C�����6rL����)�G������)&�����{+�t�]fYm
u?p����!���cK=����
�����YO�-��Y�dy��k�64VPaJ�IG�����`���_����lcEK`v}�]�`6��r�� wh���.�����{���o9�����1�M��V��u�rx�e��a���(�
�����
�����mM�(=��<����4��mJ����v%�����jrb�#��Y-0��e���^Q��=�%p,��d+���Q�UhklN��j�ZK��6G�{�x���5�{��x�x��{A,�����
���'���H��#�~r����\Sf��^��vwkg��;�y�����_;kS���cA���q�^b��G�	���m����.0�*u��2,Mt������������g%��bdq�A����6���N�gr��[���5t��������^0����*�H��h�v<������J�:�^,�����C���YP�C)�������d�A��,Mr��wN�w�p�t#���Uaj	t�S$;�D�B�����+'��=I�=�os�}�P-O7�)��b^�7���]�S0�(�n�k���Q���E���Q���@�t���Z��U��t��XbL����q�j!��5#]_�g���;����������=i���%�������>�/f��wV��9��cO)W��L������9*H[y�,}��w���������Q�~6L5&km���g���7q��R`�Ms���
��ar,��}Pdgs|�"���o�
�\���:��.
�0;c�7xj�r��x�{��~/���*���;\�D�zZY9[�~6�Ut�bp��6�h����J[�<�K�&���������:)^7���I�b�]�4�Q������)7�i���pf�h��s��{|���Z�R�+g=}Y<�m�jDC�8<���W�r���3�o�g�y>����c+��W�1QI>�y��$��5|��c��Hy���e*EB���Vg�/�zm�������@���7*�V>b'%���+2]^����r�U��;K^v�NAwS�^���3�l�;B��W��O���AY�Q�m������~+����Y��Pt�`w[�c��5��������8�~��R>WWz|����u�c�}��q��w5�h
��5D�iM,�V�X�ly�B.��^��'z�F�6QCe�KCM���{4#s�*7R����za����pk�yE�/g����(���'�X
wF_={����s#����an)h���������+b��vT��3&���OIC2"+�{���'	��� 2������������Qr������;�����[H�
��'���rh���F*���Pt;`�9�|0������
��Qb�&�����I
mr�x+7}�]D�"��z�s���@V}32���Pe^�9�*��;*M����z,��p,�K#������Gd�Nnkv`��,��5v���5�o7��n����hy+}����7kH�Z���X���V��j��,����g0=/h�mh�CBJ�:g��0!
<���t����)�+/:F�1y��Z���;�B�E��?I=�ee�����6Y�`�|�?[<�����c�����	�!���u7�[Zdc���a����OI<�[���o����J���j�0z|��v�}SZbU���d�0��'1��v�!�	KR�6��>���U(Bzd`���wc3�YSq�n,�ls�����jl"���68,�.�aUf,������>^�����V7^���KKw�X*������!�-���8#a���n��U�M��i�7���,�Z4K��������HL��nY]Wny�����NJv���������q����ok�,�����[�U���]���%��a�����p����y>�9�`!=�[\x]��|m�
]Jxy�G�+�����W��5:�u� ���T�+�y�!�&��I�����N���;�7d�@���RP����.w���(�IY�2�`[rDK�+���a��3�='R�}
GD��2K�����M-{�:S�x���H��9��\����k���Z��r��roR�udV����9�������=�i�#����:7�3���>�U�g2��� �;i�V��+�35��p`1�wl=���=��af�l-����{i��x/����)S���
,��=aL������!��a�H��j����3g2�-�em����M���KZn�,�^�S0���o3�`�{g�c���P��5}�����J8A���g�Q�������M3H(�S�w�s{���r���>����)H<��� ���N*��1�Ag�A��1�g��j�]*��
M�U^n>B���R�D�i�5]].�#/\VN(G�&#�c�6U�g]��l����u{���^ad�!���3>��3�*&�+T�u������C�G,��!%�}����\�<w��!�Q{"���g4�����:W~����,yFz*��l�%[���j���4�s����$��m�[�F_���t���8�o��j���:��I�%IgbC3j��r�J�B��:o�����$���v��{��L���[��|��k{��o]Z�35���
e*5�t�K���|�����~�o�[��b����e��j�l��P�T���#y�E���.X�����{S���g���LO����i?���h�b*m��x���1}�S^\������u7�#��@��+���!�g1IK���6i��^.g���>���n���l^Q�e���.��]���1��&�V�����;Y�-w��
u�Y�3/}��7�G����x@���#w���kX+���=~B�a:O��������9[)��M�;C]B�gk
������e��K����jc�i���������e��v�{��<��2��+�^,�l����`)�
����B����DL�'u�]�;��f��]���9�l�jk;��R�R��)��3���7ciYN!�"�a���/�����o��=�t��_E�����{�j'g6:��E]��q�(���]�A���Pl[�+)%Me������q��6�g<��RiTi��-8���*��R�n�onb���������U��Lb%�G�����U����!t�5p��XI����DTR�A�vw�w�������U66�A3x�u�kDW!i���eU�z�����Q���,#KM��g�J������������Vr����0C���V���%J�n.�Z���m���yYA��ll�s�!3}A�&q��{-X1��tk��# �S��V�f��<��3x�}���z�&.��*�w_���	
��������d~����k�:L�&����h��S������o	����T�J,��k}���-��u�0�k�q��A�������H����MS0�����9A�V���$����2n�3����o�����I��*���'��'C�@��FhQ��{�f�QI/v;o�h���>������D�Ly���O��3�h����[�A^�`�0��P�Q��!��X1���u\\��wr2��K�^~s7h^����U%��;f+����s�z��{��ut[r��U����vf�~c�x�s��HF)X��L=��W�ThyL�4-u���-�+�U�5Sr2fs�V���`l��8�L�dC\��i�h�y�*��t��r�F�������c9��Q�DU���U�Mm9�E����!�PJ�N"�$B��nz��/ ���m�/8��Y�
6��u�� ��a�����F+7*ud���O1�
�
���I��v�^�z�j�����}y�J�����r�a�e�{������Q���ce��e�N���[+��n��p��y��w��x �x�@���3���8���	�o�E��J�Q���F6�>�|�T��6������X������g�����g[�\x_��'X-�������u�x-��dm��y�u
��c&�� �uc�Hq�Z���w��
���\���P�P�e�ncc%�F(<5<�B"�cf�z������,�@\�j���%h&�.-���gc�����A����!�����U�����XI��~`r�b����_�c��*���G��h*~4���:����fR�����;5e��m��5��(_���*p�S�J�Ov�c��
E�j�c�g/]�����> v�V��6����K)xh`+$U�EC��Q�����vr)��V�#v��:5�g�^xn�U��|�B�W�:4\��3�Z1qx
f#T�=��f8���W�g���.�9D��Z��TpIo�X���5�I��D(��R��k.������t1,��[�PG�����1&�[������u{���(���w�r��e���'P�s5��]�5��^������x!�[���|L�(z_(%o{=�}����Bp7�R~9�I��,7��>�e�����2�;{f�\�W����l~6Z�!�'�j�t��O[��/c��4b��R��[ ��|m=m�S����}O�+����}���7��]Z�z?OZ��>jfK�h�&-����8/)n�
���K���
Y!�7�&��.��(h��
Jm��W����M�����f��N�u{K�G����e�gn�)��������p����a>��~9���BF��Wz��R��E��U��3q�Y�#���,��8��V���9U�d3�����<��%ic8�}����4���}u�W���={��D�$j���Rl�C��E���$SX�{C\���I�6��iIt����k2z�vd�ym�o+�N�X&l��qr�����^#�����I��9���M�c���=��t�E��B0���[�o��J��KO�����^=<��l��{]����u�Cy����z^f@dn��a����t�Z���vS7��<w�0�/a!@�h��1D;�=���A4a�w0e�)����]{r�^
�v���;����e
^�7}����<�����-���.��9H5p��n���yP6�j>y���W/�2@|��w^�9"���6(�:�j���u������S����/�����c_
����u��r����E�������Q�qJ'����u|�r��1�u	���i�f�����'��Q��;��������k-�s��-}Z/��u�]����gls;I����-^��,_�rD)���r������p�Ne�;��xV�����mB�-���m�b TZ<��������u0������^lt��rn�&����Q�����,lk
s�c�x�V�m������:�4��{z��;"�{�4��]�]y�������s��?k��{�����E������S�:��i�6e�|��2]��Q�&t�It�*Jn �k,�og]��B�X�MF*�Y�
q��97����5�K��V�=J��
��Kq��	�������]+�Er����P�L����u-pof]���)�.�����O��6����u^�Cw������6�#��u��h�-��Y���M�=�+��!������<������;��8�Q�@���"�(�����c~��.�5N��n{X����)�^����0�
�Y:d2�������jt�J�e0����xL���o��F_��{Ix�nA��Ir�;
 �����J��v�R�����m�Z�RM�B�:���
�$�M�bR���*=�"�1\���Z�����=�"����j�n��S`��
^�Z}�/z��px'tl�g�<�����S?O0���=�Ua������>�u�|zQ�K��1��o,�g�Yd�=���XC�U���:������?A�1g�wv��C�V�����7��Bj�'[��&�j��\����{�w����
y�w������+dZ�i;���:���B��V�:��]��*�4E|�������=:�}�y�]:�.�9s�_"�QI�
NX��$[��&�6&$�����{��gz��YU�{;s�S�+�y�V�N�si���O%"�8q�!��gRy���1g��+���y�����&����+���"�����������{�0S���
�='�i�A���Lz�Z�C��{tF�q&�=���Hs�.���R���O5��/ =�:�8��
��3����tw.��l�w�}���������,�(
!-�"P����,��E��u��)�������%�r.�<��[%�����mUV�6R�R��ec�z��w+_\���|���|'�S(z\��^��.��6��|�f+)u82�lw��6��
�wt��WE�%���e���Nn�F�@���o�vQg�> ����	
��[�.�V�8r��s�x�K��\Ld��U�R��l���m�h
md��E���SVW�i���8fY���Sp�������NvA���q��0�Z��x�^����b����c
:�o
"�y��q�2�n�~}�<��p����8C]
^e`�6���^�sSg#�<�Qk������-�b7]'�j{;+4 ��KA�o�S!���/�k����s��n0��-���vdYYP�������3�d�z2��|����/{v�����\�mk��aJ4��b���t	���+je�U�����9��,�y{wG(kt)z�,fy�Z�������\���v���4���w���;{���$.�&Kb���m�
%[sN��w�[}I����K�t�l�`$�V������;T���7,�k����#���������K���4E�
�	:�����	����i���2b�~�l�|�+��k~|X� ���rt������
���U��!�.��vufa�����aa5v�_p��|��b�5oqk�_v�q�H�p��S/HS�gh�,��>l���-���{=7��P�zn>���W�W]���C����{R�:H���^����P<�F7-�wZ:d$w����=�����mE�z-�
�FS�Y>X�wJ�N�n��l�G$�4p��2d�]th���n����������L�rl[���ZX
twV���F�k�2�V ��%
�^4w3xi@��Ww
���R���0�q<(n��N%�{����*[�gr�E�-��^/����M;~mm�r�CH���
u�D���������P�g7E�(�*D��|5]�R�����L��6�q�����/�xqMX��F������Z._�f]3@���tBR�5gtr���f�F�[;y�T���W)KhE`����ha�r�_OQ�,���{'?Z,�=]���|.�z�����#��;�1�8�D-�;x*��P&�2]�0(V\f�����m�!�d�n��:�������t�����=7��{��2����N��zY�W>	�y��P��TN��U�p<:J��{]XZ}�y8V
h�D�.��FU��g�7I���-�u!�2�w��otxT������V�����:�����<Q	��".���c���65�nQ|���2�B����4�������ty��l�&�nt����;��(�E�K���- ==}^����x��W�F
��V�����Y������o{��������q��{ ;�(u>��a��C��*����R��7�����q�����Gr����i���]����1^+�����ps ��Y;^r��FL.��Bm.����g��z�d��D��_xos��Wm�����x���<18���$���8=����d��\��1�����G�i��'��4U��IG=g{��W\�oL%��k��g>]�+�]�b���]Y���S���7�b����$���i\:6{����r�����!�y�&�b��W��x��3����byY�

��������K�����%p;#
����5�|�����i���������Jk�,`#����[����j�<����kU-�[����!�g8"�0�"z�+����FoZ�;r��6vp�Wi�kz���v[|6�����O"�V�"(d�D��wW�X���7�����T��-�Az� B��-���jo{y�+.��U�!i�re
I��cj��K���rs���]�e���^����Ej��l�eh����`�2�tt���\��+w����A�z�xU���h���������{������rc�iK�O/����n*������d0p�����o%O#/v:��1���<h�� ��y.��r���s�jj0�Y��[,c�cm�W�Fv ���y�E7�y������w5�������r�7�IE�ep���q2
���H�Qb'8�����4��hF�GG{���K��Gw�Z[i,��Tm��f��_�K52U���It�O8���|�WM�wO�]�p��T��������H���3~3� �����++/t�������LW
��e�mi�M����T�R\4^���n[�n�6h�2���9�]���9����&!����FP�,H���}���|m�.�+]�'���9/��}��]�~�.�<�
��i��)&��=F�K>��K
�jv����2P���s{��(��l����4���c��s�oU��-��V.n��lZ�#z�{'���L����a=#5|}������1�k=�I��=ee��Pj�������s)kYD������;`/i�G��q�&yv��.a�+}�wa�A�#��u9����U�c�������B��V1�G>(��b�Rf�F�]�17��.m^��;��il�
3�q������F����q[3�N�U����W��>���M��nh���2�M�p^8P�v1���W^��}{20!Wt��P}k�t��I:��Q|�Z���_R�^b�Cw�u+�K�k�p���os�%�-{�X�m��n-NwY������t���v���rJ��Y��o�+�8��T����Y:������N�M��O)�KW�%�E]p��x7��z����W�e�=:P�`A�z�������{=y6'y�w{����b�^/�%��X�>
��]����(iO�n��i�Z�<���������tf=�N>���}J�9��������e�'P�b�������7)��������
$TT��7Oe������'�-Z}�W{$��QV$J�A-��lZ���]QWM��jx��}���������I(�h
93]��������C�n?\i���GMA�s35o{ic�����z�]j�N��S`��_�5����W����x^j�e����R�����L����]�{u:N�{�F��>���o�����O��%�r�c8�<Lt�WL:����m�n����
�L��K�Ytpl�%�\�:�V�*�X��C����8gT���jq�s��!�$L�<�>�*�L�U�u~����vrv�e����rdxp�2���t.72[��A^�}~����[�[������c��U�I9D�}:�Pr����N�Lq�������U�$���b/�zc������{W����T����J�3���$�nV�m�����s��c�f�BY�u�W*��b�Z�x	u�@���:N��9#����z��2���V��4���#e9��
��U��>��H6��j.�P����b�4_��sl�Z�9�hu����;8�\����0u�n�&���O�6:,�Z�d��it��ab���������eR�/Vt�t�5a�
[�������m>��!
u{KS�S��v�
A6�U�^)��gU�ph����S�y]����r<�n������7=�����"�r���vswu�2�!�<�6��)��Er[L�����+�T��c�����&e`��<s�#s�A����7��$x}�s�xUX����n��s���r��wt{��A�st���u:�lt�Wm�eL������s�_m�c3���SuWq{�y����M���R��Y)6#+o��1�k����V+r�{9Re{10��o�H�tZ�O��[jKN��x��
��p�|*��g����L2-g3����Y:Au�|mjj�����i�����������@����������5F�a7l*y�B��*g@u�{�7l�M��WwF�'��J�����������y
Y{o,Z�����D�M���]g�c'�j�����zBA��<	�d"p�@ ���P�gy.2{�@���]nb��6��rTN���������Ohd�3���C����s�`&fvs�2oX|1���n���t���Q�Lu�'
d�iZ���W� �~Y�����V�&n�)3�5�&�$��o�,O,nx�sF�����,4��fV��]���Z�WV���w������uV���z�4����r	t "-�zD�c;�_�]�h0���o��'/�Wb��qX_a8i�^
�hT�%%0����t"�;�t�u������*_��������7�|lmC��pq��2�B� $<����-���h�-m|��1�I��*YV��0�H����b�fh�
���]2��]��������)S��y���W��MV:z���"!�����h^�]�T������z�V�E�Fk������UN,Fq���������!��w����W�#�����o�P�mfoUn����	5TK�d�z�J��������8O ���a|��E���46v+���0w��B5nGB�M���XS���V�6S���K�;��//���?�����7w�>3Tt��t�S�4^^[56�g�x\�`t��!tE���3
�U��a@U{aq�5H�<���X����"���.aC�w�����@Y��'Zu�5�J�F���M�5�wH��G/_7��_����8���y	����Y��|xo�D5iL�s��+j����lM�v�eKw���DG�����1�Y�S��zl������d��������Y,��1Y�{3����)�e������r��	�����
����=${����p�*��AHtIwYp�i������|�\p�ll3f���j��!:�Du:B/z����K<e7��#����,��5��>������+\��n/O��1�=�OU.��;r������Rcjq���'Q��Q�+���q��CX�L������+�O���
�T��z���!����C���k����/-L�����x��p^y��DL}g�������w����S�`��C���UJni�A{W@�p�o}�q:��^�{�Q+�����HP�=���wC��/{`�� �l'C=��������f+Sn0��n�V�=q�������\����
�V8�V$�;�$ �w�;���U���������!�Cv�����w]�!
��
���+y����,`���*>�%����\&����G�n����u��1�T!� �gV�.�t��g�S�s�yTi1��Ho��+20�����mG����5��$��U�d�����D�C��Y��G��1��m�o7����a�X�R�.dNp��tZ�����V���!�W�x8�z�UxWC]T��&{��/����;��:�G)Q�Z�}vr��o[qNk����3k���L��\��N�Q-M�������`,�������C2b������B=���x�������gK��������m�f���Tn_���M���:�~����{����+����k�{{�iW���{m�9���^�$:���p�-�**3,"5������J�nKJ�<}s�{(�����F��}��kw��e�kJ�Q�=����!�U����1�e�iH/���pc�d�����8����c��Pk��g��|�fP�)�{���Lj�(���PEE���R+�n?Zz����	�|54"�tj��������^���H(�;'X|I]�d��a��Y�I��j`?m���+(��qxn�����N����ElI�gM~����&V<�}w=R��:���V�R��\$�9�T�{=�������YmJ�{��@����t#\V��}�e�I��=��6��uM�V���+��A}�w��3�j���Q"O]s���:���W��j�5
����2��l��t>��{�*��^h��oL��q���J��>'}��U�]Z��^����LJ�w�]�e��n�:��O^Q�����_af�[�25�>X�)vjCKF�]��Q���j�*Iw����#~:1.�"!^f��B��Gn;3#K��G�O���S~��z��"�;����9��6�QFr�<s����v��t/������Q��m�&�J�����5r���`���k�if�#cE4����:���<�]_C����\��y���%��Zd9�u6����im�a��;����7*����U�
���Y�,[GL�q��{�Jp�7F��!���G\�Oe7��h�fnr�ois)�^��u�_5ve,�q���y]W�Rz�d��,����r��x����o��iQ�c�]�749���)���8�LU�vkr�X[c��cv��\Tq1M�@\�����N�'Z7.�G��]u���?��z=s_EO�1�����U5=a�ve[����v�h&�V�������\����&";���E�V�hn�[�a(�z��t,z_\�������-�4�fle��:V���b^�rk�4��#����^wv��`�5�r�����%��n�R��T�����*�[����
��������~�1f�}���S].{Qb��{�[�'��^�{��i�Ox;��,���$��/pN��G�x����Uf�],(Ek�0r���I��Y�����Y�iK�rO����&��U��a �F�|1��i��\o]�V���Q&�q-����wP����.���������edJ�zS���a����O���K�;��V2��yP�k�V
{W���w���`���b�wq���2�����|�K=��W�XNv��.�c;��7����p����6@1g����$�%_���Mx�����NrqeV�����rCh&�IG��]|��IS�����\�Tq���:u�t�N.����� u@�D���-����7w99��J#9S�<�x��/P���(Y��g�7a��w�T��#k��q�:Z�kGm 9�Lp?j���x5������`��:���m�byB��-���>6�����!V���rj�v��}f���z����h�y�����~,m��	N�������{pE[��Q�ab��b����r�i�^������r9V�?��`�a�M�v��.�\�W�	d���G�w7��S�����is����7df;����4UpjW;���S[��#(�G=|���|��A�,����/%��!W��TL�6p�y
2�P�����M��i����m��F#~%�,$f~��
n��K
~�P�a
�.����r��<Q��7ht� I�#6j[���@��G�^l�I]h]��aa' �p
��������f�h]�/:����K�axF�e���?ja�!��{m�������hN.��^	������2��;@n`�������x`���b�>�����[9=��U�XNi����Z���0o-3�S�,�_�vv��#���Q
��������&�};��KFVx�9�����;�u���n#o������\��+z�pT;��O :���6�t����4�����������K����hE�nC��e���u�%���.Z.���H-S��v��7��Z�8�^�|-S���{��~�$'r���9���1��+��{������~��f�))���R�����7��PM���r���^5Ck���_nblk"������'����	=yw{X�E�^����V��Z�vsY���mP�X�m!�2\��53��-�~*h5�J�E������5�jWq�8x��������PlLR�	;���]�����G�C�e��^��U4�LTW6i�!���P�)���X�G��9�93FB�I|)�������� ����k/3M��|]_�orI�9P��G�����U��{�	x��SS2Q�n�.im�}4�6�N�y+P<�O��yW[���WK�����C5�s���*1��^�������'f_rZ_��^���lT)z%��f�+�+�Z����k4n��@��������5�w���(_]sT�����i�R��3�����7{wt��q�����lV�4j��S#u��+��*U�M���n�j������Z���c��0�x�Yu��"~���
�y�F���X�x`Ya���4���<W{5t��y7mGkT;	���$.6
����1��������W��7������n~M]�)\���k[z9'L�}��zu5k��>����Y��]o���i-/Ls0C�������]��z�9 a��K)�1�`�C{��=�lDN�w�EY
��
�lO�u��xg}��.E�Z���[�
[�D����9�'��xUC��������j���yuy�P�e[�=�\�}
�R�����B�|��ub��������w���9@�s)�3������k����+G����ws��[�4z�!%i��CC��4^�b�v��4�:.��w����6vF2�����T7.�B{��_���3�C�u�K�Ok�S������k&�Nq�X���m������<��+6�&pEIh��u�S��W+x$�-U�w���9�#�h8r�<6.6�]��&;��w������W��R)�ka�* m@L��:������m�53�.��26�O@q��]d[��zJZ�P���e�14�����sA���+��gJ���h�L��q����F��|gL[A@S��jUD$3��!����7�^;r�x���d�Vv��#�y�->F�j�!���P���eb�3*��4�.~|f^�����c�������	�����\(U���N�F�,w���=t�{�5�0��p��Rz�z�%����~��/V�}3[�,I���\�>�>�s���%(���l�L�@���Z�\�������x+��4p�W��=�8�V�[���.�O��)�[Y!OS������Vz�Q��|��9�)���C#��T�&�tAE!Rq�8��qbE�����A�Yp��6[~x��Y��,�;�N,c�M;����:p��9
�E�}��`�*x���6���dV��s0oh��/w��v>�l�'F|������\�C�f	����i_�-�����o6d��N�7B�w���������5����N�������a��D�G��6������CO�_;��R(aV���n4���hw��qHo/����go��}�+����2�]���]�?��'r_��o(�y`�!�*�~7XC��c�K}C��(��e��v�Qa��y��6�E8n\�,y�l'��x&f�xO��JrZ���7������<�z�'�,��R��P��0��-�+��P���������^f���������]�9tr���s^3;)qY��:/�vA1h�y�;:G���������������Y[��,�����h��yq�GX��Ocw�����I����s��T��W��v��U�*�����-���X9Z+/�b�c����
��9��;��f��{�e����~��-A�w^��7�m����X�4��$�/pP�x�^lM��`:���1����Q�a��=m�o�Ak��yY�H[k���z�%���8\7�7*�E�t���vc{YH�p:��\���iQ�N�����DV��3�V���������X��.�+��3�al�F]c���v�`C�!<�R��r�y<B.���)Rv��^��ZE*�o{I+p'�a���;)�W.���k��>S��^Z�A����bK��to�y�]�[��t����P���[�]����M�����������bg��|���}wEX�b�������NT��$kz����'�A���4U5��{X�Zy���Pf0Ay�L"���u"%��\��#Z<rK����^�^�d;�G�o+A��a�nI���B�4y>�j[�a�Q�6���+x��{����������<t��N8�
�TNVT[v!la�y:�Y4�G��`�o�ju�SP�nk�5=;���sv
y�5���u�����v�K0��7�v���
��;(&�}�W�������m��q�U����:�)�����N���L���v��Qrr����6�����\�������������0��kB	(f&��\@��O7*�#�u�0�
�n��Y���������"���xm���`����������y�Az�X1�$~�t���s(�y�t�N�NL
�=
.&j�4hT��L�X��[B�tU������7�c�LicO�.�����/�z�H�*�����'�����6�q��7.���7+o����.o{�����J����>�b���y��t�y���uf�o�iK��n�v��Z���2mYg^r�]:�V���}�,����P�&�&�CBx��s;���V��'������q[9�z���gJ2�n{�]
�;9�c���{�d����=P(/�s��������|Vp��Z�'2����_�m��>cI�-��X�+��N}����6[���l��t����z�fM�m,(�������Tr��sn����jB���L'��UZ0����Y��n9��[�����U�k���j��R�5nP+��g(+YiB���7[X�s#A���Ue��WB�'-N$(F����y!sY�L]�1e�|��������fP��������:�Y���;y���{s�m���B����#���P�&z[�q�X^N	���|�NUm���426j.���4��-��n��7��V��LpX���uu
�@4���S�_��6��4����k_��Y�f�L�|������p���
���K�G:7!c�����;"1�#a�f���M�8V��naTuG�9^M%
+d��o{3���e�^\9{[C��������mc��T���jve�a�wAF^������E��8\�e9�XZ����������~2�e���q�x|s�.��{(yS�=Qs#��e0fR�*{/���X���J�{�"*��:���i���]GO�jQ|�V}�.C�MJy���2�������+|���D�]�I3�9
���������S�����)��������t���O7p'���VS�`
�5���/~��-<O"'��j���f	+z��]��:��Y�-��U|���������D�x�":���}s�"��O<L�~o�e���(��u�b�w����^�1�g��F;a�������"�ef�.zn�

���z���{�>����B60�<��T�)�\����eg^yU�P[5�V����ksQ���)v��Sl�z�
����R�Y]�t��
���FUD���G�4=3w�N�;�t�L�)De#����"����C����G��mH�
��b��{`��n��f���*�(���9]������|
�~Cx������v���n]��g���jOg"�������Y�{��g�����8�7+"w����B�=�@r�#W�"W*��:0���t���L��$�FJ��W�sO<���3�w�FK^;���3��AV��
�Ge�w
���� ���V/%�>{�����1���0C
E���1��0
6i-��v7'���c�v�*�S��pJ������������K-mQpR39�����/��f���;�58�����R������?|y����o���/��A^���l5��������=��`��7u2((���Vr�����������u�;�I�9�lG],��-t�TV���U�:)M�����A-q>
y�o�#���1#���M�.���o���EP��NV���p����VA<��xl���W�p�x,
+�������2z8��>���<:28��K��A����]�������b�������h����s���Q�nC������O(GX}��6��m�1��\���r-R��"�!�����h�PviP��N���-Ye��+/���a�������`q&�6�+�����u�����������@��ve���cW~�������}��U(!���xA����s��d�7c"����E��M����d9x����}����R�ZAa����qN�f� S���9\�W{+�f�b=3no����#R�[p�mn�z�s�<la'�=U����)�I�$x��U3�I�<�z�=��C���NS���L����O2Xr��:�e��\�#��?n�	S�|�=�m]:��O�-������m��5���!<j����2%���,��������Nz�u]B|$vWlY���E^D��;Y�����N�d]\���:����d�������m��*��|���I�xf���hhojI������������]M3�J{pn�Q2��w��kk��,����	��;N��yC[i�������M��W:���-����lXo�\��`9q��'��y[���f|����:����{��a��#���8r#�9I:Z*/E]L�3d�U�s;j����<��;m{��+����+���k5R�o5\o%��*�����K,{��r=������>`����b�<��c{����T�K���k{����;yc�]�Vb��6��cRxC�9����2{�kMp�f�����v�����Z��;4��[���m�rkU[����J��$������q����okpotd46��*q��B���f^��8�U=�N�������6�"�i���)���K��MM\�R|[���BoE�N����n[�x��\����`\j�����\���L�[,�@����t���mWt������$68�����b�8+��p�$4us{���Y:����]����z�a��{:AN��	e�d�����d�m�A5z*o_v��X�{����M%vj���X�����
��������g)��D�De��p��(Z$��Yli{��V�]W[��mA�`��+�����qs)�A�� z��D�f�oW��g��mK�]`�WZ]��*|]����t�g8������5�D���-����e%h����j���x�f���jg��	Kxm!C-�W�[����x��m*.
�a���X��W�����E�����"~�y��UW�P�<o�����u�����H7��$G.��}�5�����>��+t�zH4{M��fHf%���%F�XF�RU,��z���f��M����h&.@NI�������<��#�:���h
�7�g�(#(K�pC�*�^�?/�(bI+i	���I=���-)�v���\�������0���n����,�+�v�4	�N^Vy��VLt����f��
�y����r�a�K���q>J��8����t��i������!Yn��'���dL���7������
|�L�AP�4F�,��q����$V���S�y��K����i���=������Gt�����+��q�K���\���n:����C���^��qN7��wj�_�]Yl{~���<����%;mk���S����<n�����e{{y��m��Z{v���[;fJZ�/,L-,
Zl������jwD&=��,��qKY4�Y6d����M�pT���\�
��{�3����5���~q��~]��Y������i>�cL}G�����"l�ng�7U<yQs�Y�oyl
���zo�������K�`��7s����(]�]�3��
��ub=�����������o��3��`���o;_X��X�a\U#��>}Lu�f��A�Q\�c���w�-��*�#b������~�S��wE%�eIl�oa���T����~@�u��j`5;��z����J��n���.��G���{}8yC���k[���Og�OOOW�����`�+�s������X,����u9G�M?5}�R��5�m.��X�,���7N�d�qmg�����Pv���p!�
���_]����&�$��u��{�M�'l�8���k4�*Jx@lu�����l�<���4<�7x��$NC^�4W�=� �s^Y�Z'X���7qub�;M��6��ke�:%�����H�g�{�$��qZ^��a{I�1�@#n�mz�A����_f�==[���oH�4�2u�oz7q�ob��A�@�U}Np`��fi������f.���lt�iS����v7i�^sv/WG$������y�Ne��6�1������UL�,Os_`U�m������2�w�� ��\I]���!�ayh���3��@���-�V���$z�����}�ef9���r�|</.d����_&��/���-79�1�<H�y���XJ(�AbZ��f���.�9�;��>��fW
*�$���7������#,4�������f���6�fS�����B����_J9A��b��N{�'^p>���[���F�1D���������m��7[�������e>\�z`Of���<�nI^vx���:�Y�=��J�F��_u_:���%V�������.�`v�Y������(�z�0\K������f���m��~
��@<@���[�����y�hD9o���%��.�z\���G�6�T(����Q�#6�j��p%uLK�Li�f�'��;�n����TF/���Z�
$�9	�Fy��g�Vu;2�#���|�f�t;��a�"�1j��f��3^md9������"��(C"i�m{j�Q4����fo��cY�^h�I��C����r��[�=3�qO�xl>%`�]Y�NGv��L	��Z���?eN��Z���2��o0��y���
v�F]����x��-4��L���E�x��W�&��i���`s�S��d��p��3-�[���)�K_��W��2�3�c3�t���$����&27��D}P4�5Y�:9���59_���|+)>��:�y�[�U�LsrSt����Cww�P��c-AP�	�Vq*'+"�:6iZ5Gm�U��P����������b	O^�I.���*y������9���Q�����{��
�r�����z��V��]��Aqg��oo9<��c�^�nvIF�.yA���k0�J�6��2�������+�4��^d��t����w�~m_�Z���u��\]{92��N�e��a���k��%��e���X%y���o�$�����<��dsU�I��LX�������N����U��V�8�wY��Z$�����]�JQm,��R���i��`oc�z�E/#f4�N��C�;�8
Bcn�^M���/6��8@�v�,Om)��RX���veX����I]�Y	[�5�%�)t�u`�?vA�����X����=K��>����"cC�"�	������=�����tts�z��R"���qgv�������N��3�q�1��x��u��}\�3bl��eE����"�x��J&���L�1u	�^�L�d��(����c�;-�t���(+��1�)�:X9,���2���8�MU�oo�Ux;	�]�T3��\��6��� �h��k�aN����*;��� 2�t��U�Iz��b�a$���_'$�O��*;����[���h.���)���Y�-���������{��9:k����
U9��R������u	u������:���]�.�S��{����4Y�0���5��������`�j3.r����^�9#vR��u�V�qj+<�r�+����		f!���y�����d��/LRY�>�P>���X��V��%�
"��2SF]c�Y�Aj�/%b�bp^v}ue6���v��n�of�s��`AC*���x/e���5Z7q�wW~����AX�>�HMk�p/=�C�7�����nm�n�� �6�5[�:��`�*�b7��$��
T@������>{E��g;�X�{I<�k���^���:N���X���������F_���M��y���]�����M���x��0^�r�lVE��Y!�r��!���n.�#��������r(jQ�_���M�{���^Y�]�Zj����c��%�k[\^��oG���U�i�j(����#�WH�o��9Z�t�����rx;�({v�M�U���f.��c1�����^�}�r���c2�J������ME~l�U�`������m�+A�>�&`��5���A�d\�,R��
=�7�S���ot�x�'���yx`�f��d>��^hs�����)xG��Q�m��OYs����z�����+3�\4.p������������>w�wX�qG�k�^�|kL���8��7z6��@���c���U]w\����ab�6����6�7��$�.��+Y��� VS�����q�?��
�];R�N��wx��KFzc�Rvx�C�v���c.v!��fg:���u[���q���/���y]�o ����TRe@��;���'zTh���He����w�]h�~y���t����}=v���o ���>!TA����xG��YK�I����8+���-�����^��b�>\�X�!3�}�!Sq�����U�e�A���],�T�w�=����
��hC�������z�\��������B���F���y~%�3��"���_O�����\��3N{��q8veE�MN�(`���>1�7=�_t���}����������'�j*7�2
�uL�^�l����xm�f���=3@�L��9��-���n3��3�����],�i��^��.�1G���C-I����S�� ��������R�"���[�)���=~i*3C\�D
3�������;���������{�m��K#���&�v��>xL��r����.�gh]�����B���o����
�iv��'���.T[��r�xe=�K���{O�
��f��{����Yv��Q{q�Kpw��lv���z����	;2�.f��^:�y�>f�Os��^`�-g��5���Z�l���4�Z���b�x\�s��
<�?�mi��}��L�,6.@����^�98����������<�����F8�VY�����g����'V
eeo��{�-��^��}���[�^�P��-Y����*���int��r�
�Z�{a�4��L@��.\�
���%*��������X�F����RxV+�9�k�I�-M�q��A/\Y�����9Z�9�w�u�@j�k�-�JKVMV�>h��IW���w����vo[u�x�|����L[���=�������L~B�`>��3�b#�/�������Y�7��e�^1�����3���v/��g@���i��C<x[N{&G
�'6lz����VZ�S3��jW,����#=�Hs���E���n�OS�4�iI�	�{�:��7[Fr~[�o]6�k����J�u��T�g����yR��z���#�zr��j..���;�F���;�X��-k�D]�[���������a����a	�1~L��.�@���v5o��f�����ot�r��{�������y�7���IX�<�pqmlC]^���C�s1�o�lm�Z���U�H�'x�F
�#\��\�����~��![��hj����b�h������L�*��L�8#�y�A�pd�����V��-�v��K[��%�K�&�~��s�[����r�I�z�&:����)����+���n�Y�+�Az��g�]|��xr����S��gC�]��g.��rJ���n�#'�h�#���qG�+2]	�Rwn��
��j��w�wzvi��J����c��lE��������u��w�D�^����������d�;����z��.:%p��;*S85�'(gD7���������W^`y�*V���S7�= �>��K�R�����qq�Q����JD��_^W=R�T��"v���J,�� ��-R�}��/�����#cik��:���jS]��9�#��X}���I��z���D�:�u����f����19�J�r�[��vi��*��u�r�n��o���TU��J[]���b;n\[p)@O�>M+1�Z+���<����X�}q���;�;9��bk�gR3�����"i�#���Y��;A]��3x:��}����K����f��=����	���g�\4��ht�*F ������.���v>LU��|�3''qm��h��p�
�b��i����t5^:��o��J��^��K��38��FadI�X�7,�[@���qDc*�o 1�O
�U�<_eG-�lv�C��x�)w�M�������R���^
��EV���)� ��	�yL���W+���_kn��qBz�^��j���K��<<

�KEe�l�40T�n[��%0$�<+|��4���o�]]uu4c���0;
�u�6�UlF��;��k�rx�MY��.�4���:�!-������G>����AM�\=t)������������.b���3y:{q����I��9c.������3������X���g�i#	w��6��7=v�(k��;���zX���
n:���"(�X�.Qc�k���K�z��Z�i�A^U�T��0�-���.����E����_n_Bb��*$\�p�by�b�CX��;u��e�v��$�2�q�������v��A~S��~�+���q�� �'��r���������C,����������]���%~�����f�In�'A!93��<��ZC�W{��~e6�9+����+�fF/����w)��{<��I���>�W��5
-������OtUiL���z�-��1�g���Y�=$�F�]����i������x@���^��:�����oi]b��Rg	�o�	���0����!oR��v6};�=�Y��Nh��7c��' z�zE��!��#_%�'U��T��.��j�w����N�S�.�J��z�z1V|]�@�Bj�swl���U����EB�u�'��%:��%��L
��C�a�j���4������g�j��!����&�,��5�D.�bq�9���!�r�}�����1����;_�hw���#���Y�+*� �fy���bK�Kx��V"�Zs4 �>(���ff
�}BS���J�oX�o���u����\��,�Z�+�R�m���uK�]]g0���+��^�:���-�HT�Y������L�\MfnQ�c��c{3`���|�_�R����-f�m+P������*���I7b}��6���L���l��<�=k��>�9rV<��h�Y��z��rO������]�F����U��|�yj��U��5���2�N�
����O�����|�m?#��ju���]��|��;�ub�q��{-�|S�lG��I���t%p��^��R�Z�0��m��x��K.[Vv�m�wf4����j�$�fRo.E��A�Y��jIY����f�n�aW��-�]F���ws�N�����:��l�M���(bW�m��_����]lN���c�kt�V�k���b����u���P�W�����8�i�X����)/E��ts��������^�5���-&.�����3%��~������O�f�"Us����N�S��x�/������U���s��.�2��lq�}(���Dv|B�1�p@!l���Y`y��|:�I��DS��X�(������D�_�T�nFh`��^������q��{�����)'�%� �R"<�xBb��0myY�X������o����k�g�}��xZ�o��Igl���Q�
��hr����*]��]D�ZIl��X�U��`���3(�����]����q����=���3���A�����%t���<*K(��� ��q�ig}�D�N�h�lo+~�\����{
�N��dS{�]
%���;��>O�����8�#���K�h���yi������b�m��=�����������9]d[�..���S�pC�����slP�������50D���zxPu�3���D
�kr�����b.��Mu����v���
W�[zM�4����%�GbZ{J]�:�Kvc��Y�vJB���:�8
���t�a���T��5��K)��y|��@���, [}������{�����e!�R��c�O75<��R��J��S��(�*��2���u����/Lz��@�{�����n���{v)�6��� H�@g�}��\RvifD��_�#���E�[v+xQ6�aLu��y��SP0��n���[������8������H��R�a%$��$t2�����+������z(��K��E�0iN��m�I4��K��}��i=F���{#
�V�n9�ZdnJ��,�U�41�B,F�r�Q��
�����M�
�T�k���Q���g��)���eq�v
�Y�9�qu�*9-U3X/�;B<0&���`�wqF9��v��gN��jga���|���N���9~�� �u�K���{Z�sY�j�����c���)���E�����\�4�-$��xw���H��=.���'�(�U���:��pv,��/}+6�ni�����/�~��z,P(�H��s	&!G'Y������l��mne������p�$��p����B\K��l�{����[�����_�
m���)���_TuW.�p tDn��������p��20�7"���`(c����b����@�%u���3by*�#�l<�>+9�+y<�3�"������&�L����T(-�$8�bqA�O|�-s%)������:��|,��;���
����x���z���d�ax�4G�����'tA',{'���Z]-Klw�N����'&q����`�3qeP<�[����l�>�6]�������`���P���I�Ae��qx>���vd��I��!�s���6�5f���ZL<3���_T�7O|���[o^<>�xu^?�2�+�=[����2�}*;HmD������p����w�A�������������+,��59�xY�h����&x�;��w��V�kd�s�{��\���_#����tJE>�����{<�[us������w�����������"�P.3"r������x�%��������X-�5
�8s��j��	����cf������|4��u�hn�Y~�b�}��^���5@�!^��'�<���4��ZKx����=�X[�Tbn3�����Y���P��E�����J��R3��w#���=�:�����0Ny�1���!�N]���.�Q��r�v��y���[m�O<�mv�W�-��KVK���n���K��*����c��&?po�w�>��n���������������v�Kv5�ac�A��\���b��"9�Y
���ca>�$���=��,�y�Z�T9���h���B��)��T"��\Sn�:���=��o�.�������/�G��a�����%�A�p��8�]k.��8���KV�m��E�.����������kE�����a����*���Y)�Y���ws.�)>��������U�r�
���fj�V���J�hnlF����y5(CQW�Y�U�Srod���SG!�4�9ZnG�m�����zg,�QWQ[)��w+�K�V����"J��]�OX��F�\�����4��)�
��v� �����-yWy�W��Ikb�%S���q��>	�]<U��D;p�X�Oe�����������%�+pGp��
s�~�u�����C�9�>�Soh�P{���!_�Q@g����#��Vk���C\,��1�!X��iFJ�|�w���dcCq��ki��'�i����D�K�2C%�l��G�Ab����k@����lR���W]]�B�Yg��q�yY �R�zn��36V�Rt�g�ls�T����Z�O���mb�g0��{����l�T�������7�Q�o��J�(�|j���*6:��W�W��7��b�7����&�*B��1��8�We)x�I�Ot<7=S����ld�|����A����33�XD�M��W���{�&�f�
��������3����q�������+^�%����z78WZ�Y.��H�������)��y��6d��p{����7Y\���;V@�~}����{s�vl�t.b7QG�nVT�[v9����<BuN�����/@h�}�=��Ci�S�	�������v�!�sO�����[�<F�>�%��Y��.��������{�&h��#gn��B>p�S��2#L����r��U�winq3������k+�6������xRZ}�1r�N��k�%��Qh�P������	v<����f�Q�A�8fi-�b�&�[�R����w���v-��������X�L�)�}X�i#�U��M�[���)�W�5~��lXrzI�=����u�����xq��j�`���c��B�����WB���`�35��F��!w��f1u����9K�����Ysf��3|��)l�{��2��X����Q���������alRE`SkG�o��o{��hv�fw/��f����.���p������b(��+��_r�.\���({#����0$�P��������U6u��s����1H�yy\)^rpw��%p`�����qZu9���`�U�A������0+�S�v���z�����`X��
S�+4�2��]�O��=8sNZ����N�/��w���u2�-������&��������x�����p<)�f�����HXu�N��]2��6���W��lb�D��$�x{�/)�{$���a�����7Y�55H���h���6�-����SUj��]������v^��X+eW=�2�����_k����y��h	z������|p\�}}1M����]���S4���H7�u����=��;
��y<���l���}.�������/o�nt��Qy
�)RJ�c����r����'�2���{�U7;yE��k������L��Cv�
�Q3|Mn�f&8K�����{�GG������M���k,A��a<36U��u�����a�4�2{��X��P%���E*5xa����{�����z���w�Vc&�O��[�2�������]�g�k*4u�W"-C�E�'�y�X��*��gz
���d��Y�D���w.C��l=�Vn,e������������Hp����tsr��!�3���������n��-��`��1�������n��b�3f����������Y�����)8�����o���E]a���"�6�{�o((�G�A_[Wb���Osr^;�)X��D��z�iE�K;6(v��F��(��^�e.�@N=�Os]F���{������o����@���\��V`����;���$9RJz�}��[�}�\���<��N��[>��R�D3���T�zr�o��~@��S*��)���8�9+���u:�L���&!X��7,�U}���lD������rj\
q'za1"����v=y�r�]�3�o��t&���y�P��Y��h���p�n�^T�Q�"����k��h��00������A��r���!��7+����B+}�f�<�=s�UGR%�+hZEZ�6�AAy�R�IX��OY�%��$�/�����:zW�?�D�t����4"��:����2���s��{������K�A���\^�N���z5Wf���Q��;���7S�6������.�S}"Z1�^�/�������6!���-S��VL���38�U�N��jI�/]��{&�������RvMX*)ol
��s7�~�<���S�g=q��0�7K���m�R���R�t�SVJ��0X����=�.��^��+%y�z{��D=i?Om��5+80��	Pm��Og�Y]p�5Q�[���e�������g��/t*e
}+gy�����/����$��z�e�������c
yL*���yd����0���D�x����^�BW���=�3����;����v�����Swp�5�I����>�o��X~v�����A��a�^�{���5�G�{w���0�YRc�cK0%{
�Y����Hl�:�R���1�jR���L��F�}��T��U8*��T�����!�u���Z�����\b�t=����������r�/z]3 ��C��9������{�����_'J��R��=��t�@d{[��wW&z��ns��r�R�u�8�B��x5<�w����</��)7�W.���R��e7}�Z}s�a7����8���4�gC�2)w��U��S��a}C�'�[/7���1�uTBj#��i!���efd�O]O!eek��I�M�y�`��	e������v�����Y������/��^RgB� n�V�4Y��.�]T;$V���!�@�I�l���nO{
[�N2Q�Ntk�)��"
;(m4����	�0Me
�-�t��7�N^Z[wWO+<��x{.�M�8�����k(�^pZ���|��>������H�"��C����
�cvE��*�[��n"1%��[r����x�*�����,�y�c-��j�jI�a#���u�����Nxl��#�`Ky�����c!c}"�n�"n�{6�z���vL��qk��������=�z!���75[/#���TFB��	�����V�m�
h��m��W��n���&O����D��e���C,U���W�(]'�{sh��m�(����rP�|{V �0����f{�������(�xw=��@��
��e���!�������f�lx�k�C3S�_���k�c��l~�`����C+w�FB�^��6���9].�)��]�6.3+�s�SB�����t���Xs<���0/=�O����}�]�3��>�H�nR����RO��t�������7���}��
�kUN�v8��������AM�q*igUm�b�5����[p�V/2y��>�����j#=�;��M�x7UneW�m���u�+V26z��k��sX���Hq`�c�/��'5�!�>����l��s���<������-���r��g&�����X���|�_@9x�3)�z����i� �sU|���������/�@�)��G�6�f' ;��n��-�=�aaRL�M���H�m�&�������������}c���Q%g�E���=���=� ����/�m�X�Z�^]]�����u��H��0����Q
����vL:�](#��<��yu���s���/)$��\��
6I��v��OQ�"��K����Z����v������<8c��6��������o#��of�Hi-�mXm�_�'������?;G�n��r�E�����/4y��XL��;_W���jA+v�'X�|g����_<����X3i,A7:S�p������{s�:nB{/M-��/[����X�,�ly��
��cm���J�����_>u�\��]��	P X������E���3

Q��H�d������P�Nbq>���r����N���[��z	��m��u0z���T����kT����1���K�"����r�������F�M���5������JX"]Hw}[mm�^��.��� �3�ni�[U���p�����[.�)o��1�L80����:��S����f�#��1n��H��� �x��z.�L���'�2�y����*��O<3en���	�Jr���Ac5�!�@����������r�/;�������t��c����H7VU���A>��}���J��k}i�x=�4�5�{�A��G��q\���D����w;5�U�w1I��7����skK��	�W�f�>1�������>���SS6%���|��ym�c�8����s�c�������}A��*smb���MV���+�}����v��9,6��Nbl�� e�Fgc
Uiy�Z�^���"���V%r�R[��>�{�#B��z���B��S]��k+�5��H�8d��Rp`w����i���(5�'���i��s�
����.�>��=�8�p�u����L`��k�OW����t�igg���t72]
�,$�pERl^���cW���l�F�nFo�X7Kc�p��P.��K��$�o��+��#H��t�������w3����*�}�(R��vl):;s�[�G����17�4�#)Z&���i�;c���$K�@���$��JcDa�]�i�������y�)�'^��j�T�[p)`���N��mL����O�}�9C�YD�
���$�z��t�Za3�+x�j�������z��6�tki���7�Q#,r�Wk~���p�����
�}e�m��'Zs%u���w��Qy]|s��B��'7 ]��/5���l$�?.��cL,��6��5�B:%��f�U�R�>v����XxG��*Z�h�-hw�����2�b��g���Vjq}�g������9yV�|r��+EQ(]e�v��6���*=v�����.����
����9����T�%��5����5��%s����(;�����kR����������zVk�����5�~u0��I�����:�Q�K�0xe��2� ����6=Z,_J�*��� ����^L�;�����nmn8!�q����2.���DV�����gv��#�'yW�3?-b��|P	��7v������kV:�i����AG����\��9���U�X�&.�g�U����#/)J��o��	�+�85�Jm��v]
F�x�Q`�E�eN{���x�t-x2}�r�M{�>����t�	O���>�M�:�S��zdl^X��,��?ov)}�`�(p��L�7,���k�����-m��x�K��[]BU��z��/������W]s�Wl�^����^��8�aK��s������h���0c���W9y&��]�^�d9�=��/�����&��q����Q�Ow�	���Q�]���
O++tZ?dL�p�Z�l�����4��w�5��lG���<�:*���s���#v��y=�]�{���{2��Ua��p}��^o���7�qsq�qG!�������"8,�z%a��<;�t�%-xC��\�7�r���v.9#�8/
\C�����(x>x���I�s�8���'cwz��w�p��H"����W)l�����BQ��L������+�����h��[b��Ad�{Qb��N�kgRt��Nn��w^"[�Ht���\rm7�v!�j�E���A�6$v�+���-���z<���p���_[Q(������^}������qVY�����r3�[4�aB�K�����n�0���v�}Oz�$�\�e����=��Fa{w!���O7���s5���� ���;�Z�(;��s+''�K��N�]��gI��`k�GX�d�\���@��Y���jv��9-y�>)�@��+@��`�u{�OV&9��J��vox���/����%��
�qtt��L�gN��������������������C���+\��V�9?M�
�:���C0��@�����Q�=4��j����-��w;%��*P�M��EW��muvNE4�+�5�9�����}�Ec|^n{���g��m�b ,>�&�|�]�{]��������;�#W#^P��MO_c��W�{s���}��c]�^�r	���Y6�����vG���MPm9QXN2R�{k��p�%�}RGN2l]������^�:~���
z�����+3�C��+��f��O�u�`����X%�^TVJ��6+Y����w~�6������k3�<o�:�?d]���d1���@>�l���rM���tP,�J��z��Pe��`eW���y'��=���
�o&�i��������T��u]�j��W[�&�����Qb% �]vt
d���f��Vj����Z�v���[Y��z[�������z
@���(��U]����B(���x���M�[��>w�����O��p�+�r���Rly�c�n{U���<�5�\��]�+�V��u��9��r�Nv[�������*v���2V�p4<|������a^���k;�9M�)�����b iQ�)
k��zj|\��:���K{��f����s�nn]/Yu��$0`����b������E:��CD���Z|*�{�_ps
�F+eH�y%�^-n��L����2��m�69�TUDl^,�C�=����,eEa�38�u]\Q�������s������c�����(�[���$���+�����m<�X�!�3/���9M�gs���<F*�7�t���J�@_�&����9����
�)���p�
����C��~0������Ps�9��N��#���Z6=���/Nkl;y����7��������]������[Z`���z[�h�D�vw�9���IQE� �����6+5;���|=�Su�������s��N���u�,�������"�T��������Fw)�05��q�wlD�����VF�+��b��GLmo�� ���F17�R����|$}�^����M��W\�=y��<L}=�q��{5,zj(���
k�����N+��Q�vy�R(�V��X��6z<9�R�!Zm8��r�v�I;���]\
�[��M���I��C<���;6��m���� =O�������j���7p�1�.�o���W���t�	s6�����6����^������H��=���=�� ���\v(-m��{.����]K�_Q���n`��gX�<�y����c�W=W\5%}��v�c�� u��A�,���(�hc���&	/_��u�<�v^fe25$H��cIj��
���^Cul����<��l�g���J�T�V�^w^7���qG"��'4M.�����xRG�s9|���3�t���1l��/d�������p���z��B�'&�.<����#����B6��B<����U�i��%�o��'�3. _��O�G��8�=x�=pg�_W��!`�[q���M�=��]�y���K~2FV��<�:w�x��Qb�7��2��+��dglZ���~5u4j�N�8��.��|���nH�/<Io'd�rj~^��a�w�g+��C��Z����:�^�7d�����K�����=`���w�!�Fb�C<i�;bd��G�q�K��=e���[���e@N�Wt��9��T�[����"�<z���n��On�f�
��a�����v���F�yc����X�~�������'��W;���e���s������J�����������%���<�r���p����rm)�^@�^������`�b���������4�!����:��I>�������q�_�.��{�
RecU�	��6:�,3�pY}�����!�����\��}�Ma�B�b���d����Z�h����l�]Lj*����z[��YT���Xr��<��W
�Ue�=��6���!r�����TB�jd�w@�G�k��k�`�=R�g�D�.�����p��5��=a�����\�k��j�d7��k�k�Bv\<"����:b�sn����H�"2"�z��)'����{���b���yQA����P��d�Hg�|q�yNSK��Q=e�o��7x���:�95#�{$�wN�^d�1x���N�P�f������YZ���{'�_���{6oJ��C*o/��sN�"Qm����q<{X,]�����j�`S�'#~UOb��z�$�[Y��eh�U�%��v��#Jc����b����;�uj�/
��X�}N�:;CZ�::i�Z4���JL	��2����z�K$�j��:���P�����f��eL�jH�-��>��ts�Lm����A��>���Tj�[���tG�����t����
�t�_[#����~���k��������������:}t�r��[`!B�k��u,[���w�mcwX�Z���)[���{:i�c���g.���l�t:Ma�=�����2��#��>�#�U����%�o�{15���YO�Q�*�h��W������1op�V�U��m*�7�iz���*�O'��qx�������<��a�TR��a�z�����[�f�*��x������y�S�6%�y���vv��ME�m��qC62#k��oK���h��z^awR�s�������,rL=r�
��]���ZX������;+r�;Z�
��wB0D����j����S��=>-��m+�S]�������~��������|u�C4��[Fdu}�rhU�D��/]��wO3���)E��@hX��E��������&�1)1�*��D�9��N��������3�K
������3h%���4����v�k��tx"���[��f�nme�	t+#s5�A%�[9�5�j����%d�����	�5j����9�*H������}y[%�k�)�����x$���U� ��s���;B�^E8�����[�_���g�tG1j�1!�0�����vW/F��PuJ�(/��j���/��u7����I/�����(L�n�����O���cw��rK�6�}v[�b������-���p���'�U�t
�P�K~u!�V�-Q[��:�J	*:�yC%l��W�l�[��� ���S�S1����W��MK��^�R��<��jr��y�z����^���~�4��}�a���t-��2���dq����4�E�Y��P�b�]P�e������7������U�[0#��!��:�7C�Jfg�-��Vz�^A���1?��%�M'S���4%b{/�#
��������K�8��[N�����}�x�o���ek �7�r|�����b��'q1�/��R������:#��,�r������Q������MW�C����z�_kK��-F%���j��������cep��#z����=7���xs�oxv<X��~���Lkz�o@m���`8����e:������+�uDD��<���8<��r�pFTj��-������<��j���������v�f���r�\����6�s%�n-�����&���t���=������r��-av��������CN�Y�1���.������^���Y����u����Hp^��{�i���O���$�k/� �G���������K�F�K��.���6X0�sp�5�|3���]���]=�u�
�N��8T(�`���fZ���S��Q8�r���
�M.v���]�l��
���yx�vt�M�����|��D��w����S�6�e��j�� ����4�Y���L+�4wS(]#�.�H<���~��(ns|�j3_P��VWC$��--��
I�����n�����0��_�7�e�{5�f������s�&q����m���:����|�x.����n!�5�/P-���y7t�����E��1�����iA��>��.��z>�B���k�S\2�5�3�IP�m�jV
�1)��S[9�Sv��*�E�����0�����j�Wi�[��6v�2/�D��?S|;n��C1Ny�Px�a���]��}�q}V��)-!��V�8&��=bO[!fc�j2S�%g�'�U>Y	���,��S�|���c���
z����������P��T=�Y��#��,��Ni��R
`{p����a>�sw�fn���J���%#������m�:OOy"��������Z�V!v�/eZ���`��n�e�����m�������>+M�/8��+�����	��sA��Hh�����M[n��{�C�W��]�s8�����>����s���B�F���=c�Bz]��:*�������B?,]����C����Y6#C1�3l�z�d�����=k^����,�6�xZ`�->�,I���&U�]g���lKf��,�^|�u�0S|f9����^�+�`���( s��1��c�v�V�|�W�8&j���i����$�80z�+c�����=�c �E:���7�)Z�&:����[���dn�|)����7
ZrY�>z9zm����.h���-���"�4:��0dF.O�`��e��H���M�(U��4=U���������������7�B"��y��U��6E�Y-��rG�[�7����	�T�p��[6��i��1_{.�c����<�/Sb��l]2S�����i��x��>�0^�j-�F�g�y�v�Nom�Q�o35*�����oe���i]$��K��%~�{ly�z�8(t�X�%0"0^�������/u���SKI��L_��KszE����kYK/��L���zr�9G��z��"�,<yzB���	X�1yue��@D�{�$.�����G�R��e���89���&��4�������u�|������e�5����z�ma8)�E
c*��_^�������xdl��[�:�|G
�tp�J���y�����u;�p���T�y�����������e���S#�b�M�cF�c��]����S"���SU���q�^9����1���M�]��,��d���c��0��t��v��\f�Ss�J�z��]��[�z�B�w5���t`P�)WZ�\���h��}b�����m	BD����5�!]��WS��S�/�(�Wn�{�,�0������V�QY���]��yp��w2m[�u�yL^^����N#���b���)%��=�O�C�{�!���f���{L�����O������+�^����M�}��g�=����o)3^��r��i��V��'�����V
/r�g�[���7hX�����]��gI�!��l���1T]�u]r���;�o���f%�����E/n&s g[�*�e�]���{����K��w���$��3������\��\������
��{r�*���5�U:���pSC
�ovhw�d��r}�jKT����\����e�h�����b����Gf����1�vn2;��c�������Cop.�^�>��������n����+Ig[H��Kq�S�<����.�)�g*�r<�:U&
�&*��&'�����v���DT�W$�f�5�5��"e&o	y��`h�e�,G��/�!q�
Gw��m�Q��N�Ys�	�
��]��l��#��nS��yZ26*���|��
o%nu�9/�X(t_[�!bj���jn��f�?o*�5��7R��v^fTy������Gxn�\g�kd���������������X��>��3��MQ|:��$s7O3�����+�����K��Wj�c�m����_L���
j��f�+��Xj>u{g�C7�:Bo��CqI�����k'��nPF 5+��n����T*
��/o)B��u;U����koO�D�,uk�`2Y���r�� E��q9��-6uN���M�8���Q�S4���������qk���^�l��G<l�9c�����dj���"/�����|��[������n�kWf�nu��u��%�kk9�_;M&��4�T�G{j�T�+������	�x��^��mY���o,��(:��#��O�4��g����{t�v"��I'�Xr�=�:�����D�����Z`L��Ey����s���k�mw.������.E�,�oi���yy�P��5����,�O`��Ig7����8[�Zh\��{Q��"���h.����v�V�/*���^Y�h2��C�Fy��Hj7�+�)l�	w���*F�n�����EJ��=���=-���Gu�`��H\}�K�G>�T�Y�7��^@W�{VY�
5�P���^Wt�g�B�������X�qr������0�M\�vf
�1	GY{�������
�Q�<�_������Y������MYK��3�O�z	���g��{�������,��`V�b&�fr��9�oK��ia�q���-�N��v��;��8Z��/��P�j�O�Y.{]F��0L[�	�C�q<`+���s����z,��.�u��J�y���L�+$��@0�r�v{Ho#�f�]X7Wn���n��Sh�G^��=V&��|:�������D
�j��x�kv3loLOCwn�v�]e�<��+���A��B���Bz�Q:5��&=�{��h�>���w�q���=!k�e��&;����_�:���A
B��~�}����EW&���G�f�e����9�R�z���cn�������]p������6����8E����B��C}ws��y[zK����nx���9��o�^����pU����`X�������������O��f�(P�P ��uV�
�D-4���%O#�'.M�����$�m������%�"u�so����C�G}�kh�@��W:1h*�.��%���d���(��,S>�|��f��������"�`���0��E�
l?Q�=C��|��R��:0����I�I�j0fq�������/{���5Qu����7�����d,��������Wt�"�EO<MV�a���I��{�-�����Y�`�xT������������8��1U�����3U-j<��m�����%��\1W�![A�7������:��=��A��^����jo��1�J���u+[<��[���)���t���c�_Wmk`}�Ct<��6w\�����w�R�����PU������������v_j�n-�x����<����(�-���5	��y\�a�:%W�C����}����^�v�����W.�#�w3������gB�����=��8Gvi7�G-��xY���������8��M���	�t���i���l �AI��
�}�5L�<B����^;�>�O����e��y�-���o��[7v:��|�������]�[�=�uP�4�+����K��aa6z�<�p��'���m���M���k��m�d>���~b
�KT��8u����~]����B�6�}��
V�]V&g����i�g���	�7��`YNs��{�S~���_��c�N�Y��C��g^
9aG��.�F���^g��E\��S���S�� -�������{��-��*yZ����3D>#k	�y�K��/x.a��ML�(lU�MCS�����#u����'Ea�W����w4 e���7���{����v�~yC���Jk�dac��&��U�����1�wx����egXG|p�6�^��������E��S���� �j�!�����B�� �����gEIt�2�m����M��e��������6���z�y]���
{��#e��a��n�q�N/	�A�^���xf�J�e�S[Ky�M�u�i���5�r�u+P<����2�y�#����,{�$�ua$��e�����X��r���jw��Y6E����VpL�q�m�F��fc��X��.p��[�����6����=�~���9���Qy���(���[�W,�Px���9�}{��}��i��]�	n�K<]<�RZhh�^g��1���|�-�b�L�]��l�����J���m������'����p[dX�B�{+>iVjL���	X}���������/,�;�r�4%���n�E��Ab~u�+�"�Br���dA�����B�=
���&[�hkn��O������OY�G)je��I�C�A��6F]�4vI�<�gtN���5t�H-u�d>�[��������od�<�������R�O�ImN��g&�tP>���QuE
��m��9�����4q�"��[h_-�PF�+;�p�q��%���2��.pj�]k����-R=�\�,����������y��Q��H��7'��
�q�h��v9������4���1�V��}����^^LR���t/l����6���}*�{wb����[��$t����+���K�������j�����������3&]��`�;^��{^�<�u�R�#����O�B�N�%fuNI�z{�"��YE���X����{)�!^�y?{*%M�(�k�V=e�1���cBx�#�*T3��&�S�)�o	8X�.:w���jk�C�l�>n�b������y�6+>Uj��n�$w%���.W��M��g�d��,�[��]e���l��RmM_O�$�)b2t�&-t��cw����1��p�|J]�]���[+bn���9�k�O.b����� +��P��yV�xm������y���Q��(<�Zo�<��h����_������^vvf-�>�"��F���!�}�lM��H�OS��>�xm��D��,&�-��H!�����<|\���&\yu�l�q�����dUV�t;�Cs�MP���i��{�:�~�����f�c�����G���8N���b0
u�m�yu�{W��^�o�W�ob��1��Eik��������,]3�
n/^w�����`�I�x��n��w�k^oK�+.�����Ir�[���!FF��>ti=�[D��~�����u�^�~s�~D/(�ME��>v}���V�[��O<����������
�h}}��j\�^�v��7[�<�a�j�.����>�Qj]���N��7hcy���=�PKS�q���!��s��[��\����U����=u�i������w:Y����r<Sw�,�2[m��Z.��+2-�gX��Y��q�+��+���]-4���O}����T�m{����D����j�f]]lq�8s�C=�-j�N��7���X&i�Z'�c�f��w)����M����,���2��i_���M��y��(��2��c.aVG3g8�0s��Q���G�����{�S�1���G)����cUh(���/@�xs|��zj���=0�iP��Gd]Gn�8m��T�������B��/v��H��-���(-�����Z����,��=������w!���i�k"i���{��u������{B�]��3i��r9��n�Ss7ue��~n�y��2���j����z%�����w���o�q�8ub����-W����YH_�����M�]�k�t@���E
A�7�r/�:��i���k����u��C�D�#me��g�!$�^v�X�B��=�
����_�5�n�^��)o���^�6h]y�]J�d�e��. �:j�P�v
V8h��1�z��L��TWB��=z�.����U����1��*��/��z������>j&���_{(r�{�P�v���[;^;_&x�D��6m����&p����Bi�H�0����2�ew�3��+�	��0�YgI�G����a����t���.�/F`��u��q�B�+(Q=����K4��qY�\�]�T�U�N�5��L��}�V���,�'t�����"��U~�c�g�n�~z14�nw�W�����
��,M�t����K�kg��1���|��������% ��,
���#&�t8��S�</t�H�7{W\���K��y<4%�g������I;���<}L��T�J��/_;Xx��F��T 7}:K��&*����P��,�tk����������< F�V/��xph������ah���n��| ��tTS�vV�x�
E�6/��h,��8�y�T�5�f
���������)��R��=���N�Pn��4;�[aL�����;��R���b{$^A�y�}�m��Ol\}�:h�z�����x�E����~O��+�������LKJo�h���z����o*s�j��
���IS|}�
��J�88g��o�7)��MSa��^f����2��1"�=�(�����t�����+�e$�@�>��7Hyf%��������:&O����rb��u\kMD�����b��,�Z*�f���8t�����/y3��rt2��|k|;�}c<����>��N��qm�7�C3'ic���������������}����;r
��5*��M����7�26���O}�V�t�����{��8���f�z��,����������<��?��]z,h�Wmi�O`0A��=J��O����j=���PR)�6�)��S7~M]��\��Y/%���!�����]����(��[�2voJkr�V���R�oD�u0���OgRb�����3��Ej��C�M��h
*�U������q�:q8N"a����w�:��!�������||�M�O�+ g�Z����=��Q�h�����^�$��(��T�_�������z�4�C�DUV��n������*��[=,3�3�����������}�����w���e"���F�\�t����/l�..`U��U�}O�z�������#_��
�{��g�w��KC,Z����	������lNWb�K=�[������m��i��/���;�u�m�e�.�KR��s\NoUCZ\��m������p�j)=��O[���~���z.z�����*�k�l�����x�fvwo+���,P��1�k����\�n��}��O�����*����%�����Ub��&�')�����#�{�W�!�Vj>0���
Jc����T�/+x.����u�y�j�i��D;|�����q<�����-��z(l��:�G��"��v��
��e�3�Mw�I�i�C�5��e��#8��X����������aaQ�sb��wn�P�!d�*��/�dJ���ZL{���H�S����9��������$V5��>�%V�yN�K�"��d�Rai�Ag��5���nw�y�������|�O��v�����/*c��6A�]S�P�f�^5�.2r���p04���0�����$������"F����6��xD������n��OO\�A�N����1�53��$��qs�<�����3�ptS��(B$��-X��8��a)�=�8�1:��j�\I�yzr����5�����C}{a�f�om���3�7a(��t�;�x�S��b/w���S`�o`�K(e.��`���K��\�!��;��0#���k�z����Jz=�U_Q��	l��{y�<)H1������(�p�+�j�����a;�>�$�+�5��S��&��RX"C�h�u�(�c�RQV��q�1�Rw�h�L�J�����=3�T��ibt^#�z���@���,��~��}x��5���M�w�(��SH��AM*I���z��G��9�z��j��r��+U������'�m@�t�:���x���2�����3C�=�!�C������A��k7�R���\3�w�����2��>�i[����mL���.���}y�;o��4���5�8K����Lv���������U�KK+�(��45�L}�m��m�<�3<���Q�B�?m��W�����Fq��{F�kV���^��;�}t+�yb���v�53Yt����i1�sLv���*����|����z��hX���zL�|0���]���j�����~��
���,�m��`B����]�p�{�l�`�1��-���3j�Y���@9Rd���[����wF�E5�.�s�W��pe*����Q�:q�ov���*p��6���I�U����@��n�q���5,��9��3w�a�v��7��;�j�9��YvT��C��A���;�7G|k�sn�%II;J�J��
�AT	�y�����������(��k71���l���#�"�+�zse�;��6��x���'N�(���L�B�\T&:f��U�f���V�4�s��k;�H��ynB���]pf��B�Y[�����
��sf�!C�n���NC��a�g#hv��)0��&�E��gY�T�_%a���*������E�4����w��N�\����78����l�`h�=��������^6G~��r�#s&P�lT:7%�4�����_������)�N
O\�<#�����Z��=��)-��������s���������;s3�f�X���e ���j�4�1�[7�n�bbt��q���|�������k�s:����%��j`�tC�7;b����v�H�������8�ys��Y������H�b�b�4�'c������T����@G��@r}�B���qH}f���2��^Z�������iP�)w�Rf���!��"��y�<t�9���fi"�M��w�`}���
4��.^?x<���<�7�=n�4����dn�4����D���k��|�����a�3B<oCn��P���?$���������#�bz-eW�}���M
�s���*��#���D�M������or�e]iB%+]�B� ���Of��5I}x{q@������l������E�H�z���"<
��i�[��R$���0��aO�����[�18k2Y
�������{�3G?_tw�C.t!�32U9��v�����z����r5RM�\]���9������k,�(rv�9������cY�-s�%�)����^M����������%���R���b`�kW:e����=���-��t�9y6v���f+kA���c��
�{�I���Bn��:5SK*l��.�n}����(�U��^YQ�.��w�������v��P.f�d3����Ngo)�)�:�u�������v��dU{�el��q�{U�(�&���	O�_h���,����,-3MYB��#<�x�u��u
�4��{��T�\�e�w}}���&��w)zo��g����,1]1]�z3�o���A6�b���r�D�s�7	����'ld0)�!Y���;�V"Nw��m��������0+��,,oWe"c�t%N�c8�m�FQV^m�$�8zWT�T�e�'������V3,�r�e"������R�so'G�`X���,������&g���9|�����Q��?�}����(�@0k��M�����c,)pY�T�4_�WK'�Ho���W
P36�Tet+:�a��r�[�F6�[�
���7�g21������q���h������Z������JH����7{�eu.��aWpL�����x�)��y�mv����&�1E��)
6�\�*'���k�|e�2nm���+|�&���)Lh7��S��V2c�������M��0x����8�uk=]B��R���
�t��t�Z��O2�_I��k��k4d:,+7������:�
zp��y��M��j�Rx$�y����X�^^����5�G��u��O��9�N�v,��0a<=�����/_��"������u��C]w'�������D�T���W��yvE�d� ��*�<�E��V��5=x|��e11��xo�~�}$�5��wy���6�����g5�/���]�	�+������,��D��A{V���[�������,]���4�����1�.B�8=�B-�u���&���y\�����gav)��������`�~^�W|=���X�#���:��s���RN7�����m����&�v���ry����xY��y���"
d���L�*��z���xP�}�Q�������Y^�����N�~���I}E?{�������l��*!#�A��,�j�1���m��bI�Wt��I���S=R���^��n��C��M/^ut^����A���o���q_v����^���+3k����� 5[�A�M-r
���n.��k+��f�w�@���fz�x�����f���QR������V����n5m����e�GH��G9;x�RvtYH;~�#w�w@2��x��1�'<*���1%[��a�Z+3������{px���G�{��R�_�-�����������W��������d��	������C&��X�;"��i�>b��;`:��/g\�����wU�WS�b��#BF����}�������Y�l����/ck�Q�}���!�Q.��}CN���r���	3��Tfnx��C��z�
/q|K�&�,����{�2��y�����[�
j�9=�����D���R���Xw��OV����Km�M�0����l�`v�I�V;��
�*��f
�&>=j<P�S<��v�L�Oc�X^Nb�!��*�x���������yO�������CL��kt�*�����A���
nOT��P)=�
]�������<�Zh��J��i
����<�z���5�A����H����w�]v���#k���|�J�
4�^w=m�]������j��(D���>�<�C~8
��;l�3�Y�A�6������Q��-��
����]c��������2����<}�K,oZ�� S�%����^U}���,��������~*�
xu������"���J��xo_Q�]B��z�[Y������\�yg�!j�h2��ou�*�����'b���'��"��VB��D`hd�L�L���-Wp���q�R�P[���s	����)��2_
J��Ge*`X�5
8�}O��U��`��|���s7|V�[������~�bY�)B=���W�5������'gL��5��x��+��O��Q��s&����9��W��S(o�������^_�^�B^vb�G��WL���<�G=7��=���- �U�������-�{��b��p�.����$�����c�M
������31�����U7��}[1U;
���5O5�l��%��y��n�����%��Y����N�����CMg~�^���2��r{�����/�-������[��WE���z�{)������m�Y����N��Lc>q��� ��i�}G�2u���R�l���TC��,��8���^P�r��Yi��\/��;[�jQ��?�������r�]	*\w�G@��r���u6=[>��\�v�UzpO��ygp�������#G��X��3U����O�T���<�~��!X3W���%�Os><}�����t|����/7wjM�v��q���:�Fr�	����8��'m��b�+{tb����4
��9�o�y� ���]F5���|}��B��%�e�`rvl�
f�6���gK����jj�\,�:H��wS�W{�jY���ai�+����|6���gu0��L��uQ����U��OM���z$��O�������6nck� 7�9s�m���������ZN�j������'y2r���_z��[c�����a�^I)6w��He�HA9��F����m>��8Ay����>��e��djZ�^�ez�|��_`=sB��e��C��w��"9�p2�5Gl���Vz��������]�e����b^�Q���I�e#���0/{���S�P�3!^������G�;!�����]f��������*`^����g^o_2�r�2.�;l,}s����-������,�x�f������s�l��(����d��������������|�S�Cy4��70�Y�K��pIq�d]W=o+�[,����}/l���}�8�"�;J�b������=�2g�%=�j����o�9=z��D��C�S� ��f��Cd��J�h[�z�:��8-I�h�m�O(������s}OF���i����Qj�-�:yJ�&����>�O1�u���mg5������l;�0Q>���v���"W37S�z�{�����C�/Txj8���8.�/c�&�����yx")��T��<�N�]�X��7t$����*�w15w�K���^��$����c��3�<�Mxf�t�`����}�(%���	�����62�=5���o��{�d;@���,��0���u��k�W�	�_��s��M{���a��efL
���]����r(e��q=��CH�Z�����.���c��B����h�-,���Z�%����zej��=�mYHBM?U�M������/{�a
W��y�<-+B������M�HG��s�gZ�N�zq��m�=u�knN��&�2��w������d����K4u�U��u���;^R��L��e.���v���]���~�S���C��L���e^x��_t6S}��-��:=7L���
�nwJ����n$"q���z��u���=U�Y�{��w�l���2��$�X�y��������<�E�w8��v����t��K7�����M�|#Q���5��o�g��y�^�-s�.�n��*�f��������Mp���-�"�^K������[����X��v�
�9�t�-�c�!������L6������U�����Nf
!V�^����A�����p��C�o����_��}cP�1%�F��^!��5Y����������gd�6%�4���u�Q�����k
>�qZz������/.Q|y�������8��]z�J��%b��.4�A�o�^����V��U��9y�+����[�p�����������p���j��V�<��`�d���������o�$���N��F�Z�I�=.�P7���������<��^��!���YO�.��a*��[N�����������*�a[j�^���*���uG0���K�W#=)��S�	��v����{PD�4�<����8���1������r�
���jC �vn.�}��-����z��a<�#R�:�/f�,�������[�b��w9��������/�Wt�j�69(fWn�M�5�����h�Q����%�3�S�gotT��{���V�a����t�y
��������5:	���
��������"�7�F�V�V����w��\6�
�������s�#]I��1QV�K�OT�Uus���1�����bQb����O	�C���_�\*�m�e�Q��m�V��.^yT��������Q�.��&A:�8�X
����J���CY���.��^e�T��:���N��N���.�>�X��vS���Fm>������������l�\����S&������p!\��-��y�M)�Q~�ft\�,�Qb�{W�{��S�0@�������uT����t'��5��������Zf�l&�Q�������+F�=��6c�+W�����}�K+��+$���r����O=bw�[�bv�ECC��H5�����O��=�F�u-G1�'�4(���u�VX4e����]A��	p	b���g\�����\����	������J:�B�C�s��J��{tR�Qx��*��}�J�-�L��+�{K� ��7Q�0�=C=�{�\���d�$�}�|���w�I��g��y����	�y{��z��*eN^g�`������m�c���]d����^�	�����;���73u���V
V�������h�{x�U�s��EM���Is��S�_���Y����|b.����
��m�:���_*w����_UH��r�������'T��p�M�(��-�9�E��xkE�=��q3O�_]���^�0&��f�����5,��z^�m��]r��|����
�w�����/���A"����r�n:�[��~�]xY�Z��=�����#��B����{l=���b��>W����W��;�
�F�]�/|����s/�eQ�l����7�
ly����t;��3Q�����q��.����j�d����=�#�	\�u����i��^ ��3��W���������u&o����fP>�#�+��������#���8�?���%��� ��n{��S���!,��mPY'\�����=�!r_��/Uz�H���o������'��6Z5!���Mk��������P���\'�Hkhf�Y�v�	O']�q�5\��L�?c�U������>Z������~��Y��y!�:����94��p%���juD'SH:�t� ��RyV� �Nd5+o
����S�H����<��Z��x-�H0�x�2�]���:Ztq�\�H����B���Tvr�QtZ	�����j�,@m�(��������n�N������CejLi���^��%t����^�*9����|b��%v������Wc]2ZO#o���K��Y[�����M����:���j@�(��]��w�\�X���J����6)�f8�]w��5n�0,��3���P��:Z[<.�����c�W};�6��J��h��7;S�D�w��]����R�Tic�V1��S����ZP�b3r�qg*�*�����jSI��/&fC��n�5��0���A�]���_k��>r�Uv���\�N��n�'��e���h	/4o (�irWA��V_��JHVW���X�9%�X+U���X]9Z�R��k����O2��������c"P�J�C��@/8��Wt��S�xA�@�u��8����������-4���eQ�.�\�ex�V��i����m��c�������@�VT��!�����4��r��;sG�"�9=���+��l�oI�j�@=�2�J�O`,��y�<v��qm��|1�N���;x2��	�zBa�wp�������i�%������
C��G�G�������'l�x����-��j���a����'U�f��L�Wv^�^��W�����s3oS���r��+����8J��-t�k9C�IF���=��-IH X�uo)�n%�n�)�Zl����E+��'�j`y5�������u���e��N���G��A�������	�v���.j��6k����3����mT��t�Rn����o�{���)��;4����J��c�����ef���
�[��s���t
��+�r7�U�y��PQ�C�KR����QZ����=���-��C<������t������������)55�D����:��gct4Q�@%N���)Wew��O}o4�bz�	N� Y^{��Wt}X{�Mfk]����S��Dc��v���bs�a�glY�B6v�h��.�
��F�������,���C�'������JF����=���d�7��kqS*NgZ�CM��9tw{h�����#1��� �u���	3H�e����c�������;�����v�����K�������|����e���r�\��Cf�^y������;�d\y2�,�lO�#W:��T�������s�_"�P�)���o}����5Yy�����D�e�\v��w^���a�\�Q�S�fM]�7�����4l��Ir1`RRy��^�U��['�V:I
��p���H���Y�%����-w���a�����W���7Rov��L��w�n`L�j�	���qKo&
MK5C{�WGwE��U�b�e_�q�_7����W��������!m�#��v�+��<etnd+yV�Y�a=��bLIg"��]����r�+~@y-
Y��&��������=��y���/��^n.���=<G1�;���^�k�3<�����#�R���cx1�
+H������z-�a|��>��0�$D��WW$T�z{��\����3��U���;!�M�����]v�����U1T���L����t�f"Fc~�����/z��3�pA��5��c�/<�z����?o��O�q'�=xl�Z�������#g�F�c/�������m�+C��&	�s2J����R��r���9�iQ�0��� +O
K\o=�>*��q����J�2��E:�s��
�:C����u���`!���<�[�FOQ���7!+��i���}6q^�����O"��T��f��:T��,�Q�����0��������������R�65~�]e�o��-�[����?u��q���0�� z���g^��wNz'S�X��hd#B�@(c���W(�{ ��o�����]e����"�LUz�O(��s�������z��.3�1g������2Z<o�=|�~(��tR���}�^�t�gg�>o�8N��Vz_p<����������;� i���6�t�3L�SS�r��UV�b��>T���O��d���tqK*f���p[�{w���)[T�]yKY����x���c�&��w�^�D8�a��e�J����S1�3TTl��y�ESWR�#�r���g�`�b.�y�^\��r9��>�-�����c���K��/�����u�0��r�WxVm���������� ��C�
����]�#n{��w�_L����:��^��myopz���@����>���t��Qu�e�z"oGm��M����O>��{�f)�u<)�;,�e/#�-� �����z�������
�g�s�y�c�1����:��]����#*"������,_I��q�pX�����v5K�*~S��r��#^������5�y)DzO
H�G��r�����M��Y�����8��[z1�;\]�]A��k�����\��*I\�_$o���f^q��}Q��gCi�Q��$�xh���������	�T^��|,���xw����]��]��3,*�*�Q�9�n��p��rn���5~ta��D� �-�]�_^�R�hV�$����C�c����<u.������I�w�su�U�e�*��2�1�s��
��\C�aL�E����B�\�sj��a�W\x�u$s�f�h��l�eZ���=t���]}���djq��%���sl�4n�����E��^9��yu�����F���� ���{����
��w,�.+����$��*�3���Y�p�f:W���6mJ"E��X���/2X���[}Vb2�b��� h�.(���}N��\+�v:�UZ��������8�����OM���-�����%�a���v<*U�dtY��:��$��p���(�����sA���hd
��ds���#�%�on�C�T�Z���=�����d��4����7>�c�	g;Kra�m�
�8r���n�-Z Xe���
�����o���uvP����<-�z�B/xe���i�^�@�y7|���@����3�|���-V�W�'�{ig�Z��l �V}6��L�U�{��0e���\���gp6��f���$Dx��������&���!N�h�gD����g���a�>��5[]=-s�7�z&�v�T��/��m��R����N�����,��f��_<�;����a;
������d^1�yN������q���s�&z]-Aa�FU���������Zj�b���/-�jS�'U��!v�'5$v��K�^�}y�PX�k��Tl��j�6��=���v���S ��9�H��M35�Fj��K/�LF��5�Uxf�������:�z\�d�^Cq:����^A������t&�<���;��^�)t��W7���o���1{�X�t�z��5�gn�:ED-��.�9V �WU{��.�>�H78Ny��N����K��=^��������a�B���59��{��j���=^�����.���E96��aq����-�s��T���Y�l�������uk��	��z!��
{�5����(��f�:�����#O��]���;A�z�^���L��n�=�U����\��w!���{{��unD+�$9���{�[�WS\������7�ws/J�m*GO���(m�����������>��������V2��Zl�Q�k�y���h;>�4o�'��m_�V����`�i��*�D�a��	��@g[����_�,���]�my(�SV����,//��4/`P�H'�d�)
m���t8�2�neFt
�7f������*�C����4N�8zX�F�L��O���h�8J�@w��/�)G7_�$���K�����W[<t<�9Y�le�tq���:6��Pe���z�{��^�pS���f[�7���5�������M��������y���;��o�!�Xh��3%�V�>�+��!^���3��;vETY��Z�'u��X47*���SD�������~��x�OK���k��;#�"���������)�g����+���qW�R������a����\������v=}.���k���w��4]m_������\�;A�<����=���=nr/�d#��������DW�c�gpy`��|v� 2O�&�V��5!����u:QA��5�H� ~������;�W�b��8@J��4�To��^���7o�.b[0���T.���n6EG��������R^��/�&�
��-"�����1]����������{���^�i_&kHI{K��]A[`!"?���M��^�F,���,��l�/�v���f�*?b
�J�3�L\UW�7��������/.���$}�9�����Z��)8}na�Ck=���evA�a@����^��������j��	���Py�2���^���'��r�P.�l�xJ����}�[�n�{��w��.Z�I!�C��.�l�7T�r�����gRp�{������O/v[�1��������T�WE����^��Vd�v�������#~�������[��
��O{�5��������m;�����������i�y5�����Fl���r��]�s�2������XJ�����U��|����:���)�R������
����A-�e���]E4"\UJ�.�E!4�U�0���KC��W��=�|��kY�Q������o�!�9����]]�_�k*K��x��\"��u�V_"+O�/;��y�.\Wt
�i�Xp�B��Z.e�{�(J����[��������{����NEn�n�6#=}xg
[�1=���������W;^�c)W,&��a_d�E�u��e(u�����ms2��E��YoU��*4����������I����E��.�:;�e�Q-��<_/�j�������A���c{�����+FM�������n�(�$��Gz�&��V�X*�z-������2������q���4���-V�5��}	jZ��P~��z!�q��G�/vInz��Y-1Af����
����`N�XZ�VP7��-BSNv�gNl��q`��r�%T��O{VgX����Mi�u"6������q��I��gud�28�qw��f�2��s�j%��b^?1MT�����
�?�����{��������T�#"CsI��t����^B((��I��j�%h�/]����������nQ����u�������"����3b������p�;�����z�[��v��V?�w�K�U�GB��k��]FB�=r����8�+M��p��Yp"��x+A%�����P��'���]>�m��lG��(@Q�������'���T.k5�ik���E�1z�2����[P��vU�����"%\
 ��X��n��8����������zx�E�R]�
��rX��_�T�Xj[�o��c5�����"Fb�EW��/��<�u���_��QA:+ez���1L8�Pr�47@��\��Z��f��a����w/����6_u[�8���?uG�8]�R�N������x50b��D7��aw�E�=��s���~���f�������Y%�������:��/�s����>ENu_���y�V���A[������lg�;�]����2�o{F��z�a&���W�<�X"���_�O�[�������P0����m<�SWLn_�uZ��������z���
K�L�ICq�>�v�4���w3���3v��+Z�_=^����A��.>��v<�d�o)z��;��=bE(�����8yb<xF*��6�w�����yv�GJ�61�D����+r����uM����P��o��w�����QO��� ��*����^Lb���Y��.���i����t����8����n���;lM�.���b�i%��ee��O���,k�������u�{{�B�UGn����|���Y�<���?
�5 �I
�9���_���_tu\j����F:^���]w��;����q�7[�����%����kt�&K�����r�c���]�}=��6=�Q%���9���U-'�]��EE�m�I��[H��+��
Wy��<��_4�\^x]�k��,|�i��:��x��;����{��Br�G��,-�]�����	^v���U���h����|���sN�!N�m.��Lu��R!��T�.�� ��fB^�)�����~I������|����t�+�*n#<{�}��7��F�Cx�(q'W=_�t�{����������c�������:=Z�H����fRQ'KB�x�^P�[CT����#wv�+�S"�Hy]<u��z�U��x�S�{tk�9\�m�{~r�^��x�������a���W������D:k�o�J�������Q;���21�w�n$�+0��U;�GX=�'�����Q��3���=����c��s���������.���WWZ?{J#�JwqA"��+-+��>����E�����v��B������^���WW�c���w�����o������5�{�xR�g�dW�E��_y��������y�|�(!
�lu�=�X�/���X>��w�0��y<a]Dq��b����g7*��r���K:.9d��cz���m���=��j��R68��'������3��G0��2��9������def���G������#�<���y
�}-u7r��7��s��S� Y���e���L��O��l�oH���l�9�b�`����x�yy?Q��Mu�6���[�2[\�wDI��R��v�u���`;-�JU8�p�x�n�������g�z�����\Im�zhs�U���[�=��+nJ��Q���O�E�R�(�Z���BR�~�U����N�i��YEU����^�,)f�"�\�o/.rZ�)���T�g�1u���p�3k.��mp_���T4#L�+<;|^n�GD��a����)�N�D)����+ee`^�]L����2EK:����
g��8��,���+��i?a`�g�yr������9:9��b�����l����m�8q�@��_X�zvqN��M����a��w#][4njV�
�T�x�$����}Y�
�3k65�����J�`*����{��:!�,ZU\/u��NSIja���UP8J^�1�x�X���G7���G���;*R��g�Gd��TN�q��H�t��������Z8�s��l�J�W���m2#\]��]�������"���`@�����-��sz
S�@�\K|��I	>��������QDe=w},�q�6�Y���dR���t���. {X���$l�6�;��-��#�Ghs���j�c��j5q�g��!���#��Y�sb�Ji��"D�t����)�I0��M6m#��m���yD�����s���O_�j����SF9����o��t��]��':r�{W�#PKkV�����J�L�M�=���
z������J�����m>�}�������G�\�L8�5FMW���P�u��
��<xi����?{����(�Y�|Nf��/���=���=Yb����c9�E�	=���0�7��t�^4�}�N�}��2����y���S���7�
��U�}�]�Y�<C�(g��~�te�}�a��`�Q
�
��w#s}ZFU��6+V�)]b������������f������wA{Q�X%��E�G��r3�&�Ju���_yj�1;����Z�DR����f1a��t�vwXw�u�E��|Lj�;��;e�����+�N56������|`<���^R���������e�|�t77���?I���{rj�8q>t��Lk�a���W�~�^����d����x=n��/���%���,��}d5mz��&��_�TU�7����/=z�x����[^yxik�#��^������C����B���wJ�w`�����4��F{dlOe�4���Gc�m���m:T_'HN��&��[~�V;H
��:������om�WL���e�L+2YY��(Sbd��2������U�������=o���N�*+�blj��?R��4h5�U�}���;(�a}��2�����_��%�g3���O����"C�z��	E�������=����=����%?\�b�w�7�&u%�����I>���A����J����wv�Q�/��(E��y#<�;���b������������0���X�,c�I��>o����g�*�=%���`���2��ac�F����Goa�P����ck/���$����el����sTCDQa��amu�+>��K�x�,�"	��=n����"����7�����n�Q��vv��4�W�51���{S�sZ�2��e��������J���>-��Ga"�L���F�]�q�GK�c��w���)Y�`e�cc�
�����!��.z�����T�g�����W��x�]��aEuT�`����k1���}�
m�=0�s��g��F�Q��h�����x�����	���s2�L����F��F|�^a��)�M9>���<�e��z�F��+QY��x�p��ZE9g'�;��L��w5��&9U}�Pt�)��=��;���lA���'j�$v�����w�E\@�G����"���mc���'�YJ V��9��:�e3<D�'Y����{.e�����n�xQ{�g]{�Nw���g��X|\b�P O{������.=�}���V�+�"u��U(�$�1~��)nW�����M^����Y��n��)wU�;�i�����0X��7[�����R��Fl������� �����
r�����C��;����U��s���y!���)'�Wvth����:5��`���x�$���3����MWk>K���=��w��h�xGsRX�k��R�1LF�������a����-�*{���n��<�U�L�s��%��T.oZ��������}K��I�T��F�O��~�SwyV�\�?5�����k�9�^�J\W�h9��x��'�|	������ee]�����*\V�0��.]����a��S<�1o��g��Od�i�"��g��c����%�4X�V��F�Tz��1.(��j�KBNrR������������|������Y�;=����3�FsL;	HC:�0v��A8z�P{u�b���&Q��X>�^���Zwsq���>�REJ���rbW��nm�c��(�S�������r�c9z�\��t'}gA
����2~�)��Hvf�X��;��������x^vyCn:�c���v�D���w������[�5��t�&���L�Y�/[��@:���P2�6����j�lt�����/5v���e�����c��|C�z��iQ�}*F�w��wAG*�J=��oSFz�1�j��^;���w�d�v�4'���8����&����|wF�#]f�.�.���R#.�m<���hB��
"��{��]��WK����l���nf�50a�"�,��<i��:C�L��o7����.�������G<�H�t��mW���y>�M��3ya��7|a��e�u�����]�#�u��w2��9���/W�^���7��w��l1V��a�)%���a�����|��������9�f�����`B�d������Nb��C�������*���XE�/�[��*���B�T������`��l���72���Q�w��x�����c��3z��]��{���������NIw��I:�����X����m��m�b6���VMe�'[x��'g�����z<��u�����n�n�+��g�c�"����Od����,���v��0��p�����������BV=6�M��{���:���E��s�i�~���4���G�5����F��M����_����^�(��~Cau�,��a�i���=b���l�D1qo��T��.l3K��Py5��]dW0{fOa�X2��aF�M�
�1�_��jt�����'���S�����T-��p���A���r����J�������*���v{'U�C�����ii$eO��wxI��k��.������[�4�p�+
K4+7�f�E�������*}+e���Q,RL{Ge!����.��Q��Vw)t=�D^+�e<�})K��.L�H�����,Cmv�a����rL���=�&,�)�;��+w���T���u[yl�!�^�v�2�z=�%����Q��|$�([�;���F8��[����d5,+9�^-��`�����_������W���3����;nZ9v����+��s5s�Y����5���=���59���M������������4i3��������3Qo����e�]�z�|���w�.k�f0U#�}"*{����E��H���c_,�������sd�b�q����=���T��p��5����e+��F�\��78��q�s�dl��-�I��#��6<���zE3�I����R��O���
�<oX�$��p�(`����W���R�|k
9���cz�?VI:��:Ve;�d�8�O�_4;j7��!qe�V�t����H'����0r��:�vg��S�� �9�X��i�&a�������)M�fb�z�W�����/]��N�M�����?4�h#
�;7���^�*��3������X�?�]6z�����n�uC&��V)��Z����L���f�m'PR����F��=R6���v����x���d���15g��V�h:q/4X�9�Q@��G��L�nX�fAd��f���N(v�Bf��q���B�����f�9�W�m�>x����\�]��{{"�wC������'k/)L���O,$v�.H������9��|��<�Z�:f�8�<����������&/��1<p0�Y�QB�Xme�<��2j���~�hk������"��M	y.�S�L��i0�K��Y��wM�^��B���
st>!���%�������V8y�"l�Q������hF�].O�w4�������&�,Yu���p�P:b��v�4/"�j�� =1���sr����NcNF/����q�A�So���X�^m��'���2y7+R��M(�cPySVq
��
�������6�+z����uy=����Z/�{���I��O/��������
� ����7�'�V��������pf������>�EUXb��9�P�v��YGn����f�����@��m-�
���W�8
-����w[
�����+��e���&5y����'��AzL�����
�S�����X�h��[����W3�
��H4�8%e�E��t��e��H���95.lp���N��B�FU�~G�2�.��%J�r$�#I��U�+IS��w�M�F!�X���{�w��E����T������!���i��j������|jW���L���n�����*��e�-�uvvc���r�wP��
��bx�V�l�>���_���c\�
BZoS�VfZ�o��D�t�>�Kg7y9����\�e�=�:!u�����������)!�1��7�y��/hxr���V�U;��V=k�-X��67)�m��3<842��F��}@�����������Au���1g9��^_��Fm�����|�}����QX�A��}��g5���sya��|L�:�����h-�ud���y����C���:�90hI��zL
�\e�"yn��s��Xw�o��.��s;9���]���)���/?�Uz�}E�k9luN/4'{����2��}�k����l*y��U�I��=�hr��:l�o����]Wl^�����������B�n����2�������*/��9�r�b����l�eZKc�w]�0��O�c�l�U����y�.�w.���^�x�4��O>^��B����V�����O��:xb�m�uQ�mICp�fw�g4��)�|;��]��4����FWn�B���F�\��a���x����L��*������HE�:�6�1_d��������-�������9]�8.BL��MN,p�<�}�iM�|�N�%��/'}���N���
�g1!���j
��������f��+H�j�t<�65N�p��Y���/)�[���{�e�L�&��z����dN��v�$�
�0��N:`T+�9�T�������Q�Y�!���]�K
Z%Z<VaSw�����:���d�m�����b��c&[4�m�LU���A%��G\B�.��n�<,D����Kj|���~��R���md�{��V}�:�a�%2g]�F��!���P~�`�J��W
)�~�������)�����yz��H����q�:%�dM�uh;b%g)}�856/��"~�vx{���W��I.�hD�������UY�3����]����e_a����c,i�K�oj���c-�X�*��wrR�tJ7��SJX�����n��s���2f����������}0����M��D���i@����=M� )��a��>i�r����=�q�x��W�����X#������:�~���Ys]�������hN�r�
�:��^k�K��]CQ9�����5!n�j��>�����/1��3��Q�fV�l�����LL�v_��Z>_5^���y�LDy����E��>�,<��k!�4�X��F���s�x1�,��Fr1d�s1��aBR�nF�i*�|����(p��:���
 ��� B���,�/y���}f�wY����T�,��E���%�e������8�
���Y��/6����)����]:�v��������(o�*��ieW��m	PL����w����1�y��<}���X��j��pXS��"��!H;7�"����g=��]m��s���U�[����b���z_+�6�)�T�t_#bg/J����.��)�u�
�r�p�pF���R�n�d�����cqO)�Ot���S��U}�]NBj��[W�d=O��^Zg�������G��i������39�`,V2c
5��:q�����uS�{ ���f)R]����
q��V
-�2B�T��g#L�)��>��8�X#����G�)����g�nQ9�#���n���D=*��8���ZJ������l���OA�%�9���#�^*��~�4�z�o�1bse�1�����=���Z1�~<3�<(B~G�9���/�LR����C"T!�A��L�C)td�gcA$���r�'����'��|�jWc��o�����ov�pX*��I�o�28���n*��]��j,�s��r�T7w��lF+����'�(�U�����x�����x�q�����wS��T�����<����r�',=^c�Q���f/���n��������>�v��S
�)��������	���}4�>3=�8���
�]S��T����=�i�-f@U�S#�Eip��:��a[
I�kh��'Y�!�����sm�e �N-�gU�v:uZ��S~h{u�G�A�;����}qJ9������4���2��Lfd/����^U���������J�]<��,9+�'�f�*��J��]�p�����a�$]�Q<J��!����`����H��Y[n�	v����w�S�gy5�fl����j������������j	�gT���&15Y��g�Q�v5��|'�!yIB�z��x7:�u���Z�~����*�E�j�{(��5*�Z���9�/�L�A/Z������l�ic��n�+o�n��f8N��A���( [��I���%��(J;.+�����v)���(l�Sh��C(�P����_lp�R�c���>�M��uN�z1���gy��n��/9<�����e{&�x�V��h�v���z�
wczz�E��r�2�j���oe��
]���ek�;���oX�"��+�g��m����w�������x��Y���l���������1��{)����n��������37o���s�z�wz�i{�u�����=4a5������b^�P�nv+D��F�gT�5�
�[�������V��>o|<��c*�T��z[���5���&y-�����~��kg����k������g��
X�XK�G������w��^8��+�=��]s�mC�����e���W1j���_����nG�b�Jz>�o�c����C��sF����/�TS��=�<�X7�]�V�����Elm�^�d�M	���+�y{����4(vn�I���P�g�6����,������Ca���^n� m���;���a����E�l#��V���J��do��6�����iG���@�n����cR���++D)�=�������\H�K��i8��!�� *�[��7V���u��U��d9x�F��{���D�53������
��s|�P"�{aF�����:�q�h�tDh�_t2�O>Z ?_����R��u�����v����Sr'L�I��YMTo�����!����!{S0T�e�a�s=@g��������2	rJ���:nym������[Mw�<4��W.���V��\3�/^����u��.b����J{���
�����R7��,����i�! �t�>lod�������L����vM3��-�f�U�:�amI����|pQ��BZ9sa����~~�c��������x7�.� ��u7J���'l*��{z�R������p\8��:��n�eW9�����(��e
��M���[�*�
�������;�R��F�>%���E���87t���$�w��;1U|�t�V��D7���lt�$OD1i�����f-�]�.v)��L��{������1����>�������[i������_8k�^0�:#�������f(7��D����1���Zw8ryZ!����=I	������~~�-��YV��Br��
�n�������)���Vy�']��o=�)�#8�	��-�����}��FW�w�oJR��q��m$����|w�v��b`\�_u�bKdf�m^��w	�5����T�f��{��-�[!WQ�����S���������
c{�r�S|_.\���3+�I��W�����L�T�P��~�T5@8gL��o�T[�G���d+�1�[+=���9:�Z��/��[)��`�W�v���W3���g��������j���|���v�4R�_�_�����a�SE��7\.�L����j��y���P���Z�1[:�n�L4��W	Z1o�{o/���C���k�8���.��L���4�����g@^�H����u{��IM�����"������
��>Y�q���?G'Oz��%m�O=�]��&6e�=��D����A"S�l�����m��K<
��f����\�������SW�D�H�Y�2/X���M��Q}h�OV�P�.�=�'*49I
:������+���.�X.#� ���@�����f�6�E�j}����i�]iT����L���C�&��<� ��E��������UPl�+]c�]3�'�o0�c�]�_@c��IB���2.*e��tdN������tV�c�
��.m7Y�@[����Q���.q��)��Rp�f��:�{��9�@CS=���]jL�\��M��H1��-��8����:�(���S����V]��W��o����V�0T��]�co5�5X��Un.�j%gP��?
&�����L}e�S��v�@'2����;#���������-}����Uu.:6��	��#�X~bz���yk�u�f��,{������o������g�f���`W��i0��L-=��6���a����u4���U~�7����i^N��a�I��=��TL�Q�(������b�-������=��C�6ql�=i���n5W<�U��.��
+%o�p(
�O��]����^��!�}�Q�_x�@�Zu��� ���Pli��6���2��8��������hL^�Xl�����k����Z<�29F�VL�����}�))R�ha<�'��=�f��+��\y�������0O�T<s�����]��?q�
�Wm�]���h�������E���^�������������y���o��?:Jzd����{�w��f���5����I�C���F�^6������e
Rm�g��
mnz
�c���d��
�_�ld
C[Xr�.���S��6ej�U�6�����x8T��LOYI��������x0<!r���AH�}e�j��Y����Z31�0k�����.�u�����L��S�A�.F:���v����1��B��@�-F��:%��J���!�����s�LJ!���_�_������{ss�fDk��-g��^o�^n5&�5[Ycq�w�j�V]�:Z�����y�Fw8�e��@t��[�<�����_�pq8����ym5�rZ���(�*)�}�d��8��[�T��M�3�_��������D�F��S�N������\��cy���<c�m�����V,y�l�y��C���*����^�G������U��������~�&j��z��
���U��Pw���J�/M2��2P�{�����&&�^���X��s�t�&����9N���\)�n�2���um7���
t����s�W�x����*�1�-�y�v�*��d�6+��x���%T���qA([�[��+[wO�+�q�]��_u������D3v,}�}��>��/�)�*�����'�k���~<�s�VSf�.�
N�M;�����7�����ee�a��]���1n?��%,������T)E�������q�v��=m>N�����w���[X�vz�8;X�Z�r���7S)�X{�L`;Z���������m�
�b��
�9��}K�o0U����=l���W�D���+����P��:�z-9ao3� �������Ie��������n��~�u�yN�1���8��I�z�]���}pyx6��Qa�w|�T��a��b��3����]�5���ao��� )wxY�����N����w����^�����O���6��M���������fl>��%n��������<%{���7�K],8%|��5)��A��~��G6��<2������#n���	��b�(��2����U�k=y�]r���Z�0��Z��]W9�e=O�/l�����H����.`���5�i�x����{���r��Hyv
�i��x�q{�~�xl�b��������Z��[V��mNP���>�����w�SX���V����r������dZ���G�"f`Eu�LX��bD���Nn��y�7H���:�fo+G��;�"� <4,bz�=S�/�l,W����]�)��.Rhe�����GU<���J���bG	���.2P�C1�5HS��������[Y�����l�[^��f>�D)���!�H�x��+U�ea�lv��X��y�������b��A��q�j�
������B'k�r�%F`T7����=U�{��
�Q��>f������������M�k�b��0"EugWN���v�4��u4��W+/�E����8og�E�����=���Uk���e��2���:bZ����K��V�	=o�4�mQ�\����z��#Tcf#OJ�i�������G3��@�<.��6����m�h-�w�pW����z4h�1�k`l��:�\�p�H�M��cLs����aP�(pz��c,����i9���:P��g�d)�����~�ZhY����u.7Y{t�k���Yslzq��7�]z8-�gE/��Oe�n)D�!@�{g�\�z;����n���	�3{�7S<��!�������-�y���N
b���{A�`��
>P�g;A�T���1P4Y�����QP���S;�m{�Ob��@W�����ut����f5������[kf1��4��8qt��zZg�X&�-����d��h+&G:L��)�~y��W��[�H�
�Pu�kfST��bC��"�qC�b-�V�C���<t������>��9�WN���UR�{+s&Z��r�^5����|^�����sn����������F��Z����Q ��)�����!b�����P��n�O�:�y0,M����]�]'u����F�(\�l��1����AV:���NZ����n��Y93���v8<��g��X�3v���T}S�y��J���$�6�=Tz�xV��2PMC���y?����8c��������D�����2������wXr)��u���u_���x�b�c���W=7�	=~�W�����w��m�m�����*��g�}�w�o�M���F���q/$��6I�
�]!;�������'�:	��[m�t�"����P�ZS�J�3�f�:�Q����g�2n�����~;���������n/�����7.'b�H�n����(n>�An!�S��mX�����3qb�6:u����6��'�GL�����=��z�K�\�L��CfZOpNfF�B2z���B�#\�j�db���3�U|�����L	�X}��w���]W�T^�f�i���E������yT[���J�d�������)!�����z�Fu�2�6�W��g���_F��X�j�Ibkb���u�c�Q�U�������}��w����<Y�a����������z5��f]-�0zR'���	�i��A�t������Jv'��������t�f�}��#I��BQ<������{#��N]��J0p�����bp[��+Y�2p��zL���G��-�c�/�Oi��s�nDo���7q<O��������*�.��������a�{�q�2��p���;��{zi^����Nf�W���H�o���vZ+:@NXq!�jv�����z.��F�}������WyO�A��gd��3����]�eh��R�`����&7b�Nk��v4����y/%��"Z�
��,��H<�XVx;j���|�e^���)�wu�����vk�f�P9�7i]��_Ncvm���"�;�k��_��`6��)��TT=���cIK^��vbTZM��z+�)��XZ�R��_�����^�����i��V
�������W��`���;�+!&�j�MOm��7z__���)��=���{od���D^�0���_�~�|p��5�S�v��[c��]��{�
v�n��_�S��!����:�f��m���5<���Xkq��5���n�^��[��3���99�V9��*'����=���oYa��j+Be*�r*�"��@�6bM����2;
i���:q���w�5oq�^w��|�q�������N�+��2����^��l^��b�k�(��Ej�M>�z��}�z���h�9�	
��`n�� :'Eeg���f�L���I���{�/p����|���a0���U����eb��&B��U�u��3��O?G��1����=� x{;�oR�E�T�x�o�c�#�?O�{2=�1��6�m�����xLk��<n�-3��q��NM��{�=S���?kDJUyZ�'�#��F���~�~�_�@��~�~}={)��gGlV��:l�$�����Q�Z��dd�<�w=�����
����_��)��h�Z8�iUW}�/TQo'(��g_b�MK���w�v]�fm��#��8�g9��"�s����K%��Z[i<��4���u:�b�9�%�l�b-�#�J�KHL=��w�.�H�7���g7������tz*&�J:	�3������9v��tlT�;u��6��>����I����F0O���}D�H��4�����PK�e���f�>����c7%��!��b��=�w{��%k��o&�}�Qf�.�<UX�b%'&J�U���A����$�����/GV4����r9J�7p��YF�	^S��ft����K=j V��|2�5�%aGDT�?/p�h����@<+��g�x���\��5R+|F��2��S�W��j���O.;����p�F�����r����mc���a6}����m��7����<�B~�EX����!�}�D�)'���6,-n�c$$�m\iu^z3��p(ha��4^5O���i]�����2�������h�x�GX�&y_�����g�g� m�U�^�X��5�����'X31{��}V#y����u5�:S�����V{4C<�w���>���:�*4�EL���LD1o,���z���_3sP���S����IJ#�GQOY�|�V:1������u������VN��=���"��t)�j������z�@v�(,m����M�|1|L
���9%��z'C�� �X�>�����w�wL���W^\���P��7WY��%��-�r��]����!��k����O,�I���kz������'��.�N����"�ge�C���G��b�#up�v�� �����1���z���~R��Ed�s��hivKc+���-����r�zy��'ztlm�Y��S�-��]?u�������{��q�~U��;�y����hu��5Q�\���y���e�8��6����j��r�����|����Y�1h�|�]��h���">:�u+����,w��������=a6�h����(E��H�O\lz	��w[������.eI���j����u�3��G�|u��P��Yr�+R$N�v�9�a��� w:�]������9v�+!s=�h��[��b� ��U�f0���;f��n
���:�S�|DC����!�E�!k��]���6����:p;���4�
.J��<�"��/��
Wr��V��P���]���Xqhl��p���9�<ut���f��5�����g#e��U����y�W���h�h�M�GJ�Rr��*^��__���lty���b�����(Y�nDG�����3��Wa����-������{l�>W<�rs�u=�����K-�����._^����{�f����K�}�a���*�]�/icf�����-���^rxb�����	kc��9��W�������h#(4�*�������!�.��{^?���[x��+���a��Y��dvO��Ow��4�H8�y�\(�Kq��S����^fmdw��B~wK&q������q�qj������Tp!j�\��,���\���k�]�������������v\�x��TL�FA����3��9���P�w����/�]�%�/�x����<M�8�[~�����9@,N&+G���`es�/I��!���;V�������a[�WCY�v�\q�pPhY�����)��5'c#���,������AE��x���\S�R���,��<^c�2���O�4�S����+����Q�Q���H�����S3�J�lm{6�%F�w�a<�[�������������d���}�2�M�W��h��*�<W��V�4K��k��I��n/t���}��/�oZ���>y�}����;�2�g�6�$f��6w3���7:/�*^�N�T��Y� ���7
]9�����G_�r����l���:-�P�{�����
7��4)���@���N7�G�<����N�4��c���w��k���{z���f����MN�������V�^�;��	���{����Wzv�OE_�B==�T�h���U�]�<����@s�W�Zo���{��b��H~��"-�y�������i��T+�E�T��u3cs#T�-j���K���i{����S������uR�y7 �	�\�yf�����5��m��(��5���9x��{���}D#�����z��W�������2��p1%�5Z��W"���\�����.Z�W7}tJj��j���v�h�*�2��p�g(�^���>C�}��5���������-Q��F���)�K5p���������������O`�G��>���W:�>
������c`��F`��A��<��9�S��J���R����=A-Lf���w�dk5v�c��	R�_��mz�^z����;J������sJ�
U�A��I-����O3��'U����&��
���'-�O��]!;+3�pe*�n�����Va@�-��P���v�:_��.�Z�Q�F�M�^�4�gd��OW�WS�u,4�`��E��g�v/+���
]���[�u���Y������YE\#�o��V����@�3r�g9��!��h��pa6��p���=�C� �Y�P��v[U�zk�w@�	��G�`I��
=�)%��cy�w)	W���W����r���(tu��B�=l�Y�WW��m8]��j�.���.[����,�r��?q�2����I[�����L���3���%�>�}���R����	+$�a~)�����L�2�h�r��cl����[���ze_~"��S���xf�{o�U�����Is��A.F��kz~.�����_���@`d���2[�w%�p�/�$�S��vj��(��Zv�K�`�?c��+�v��n����Q2��7�� ��v��o�3b�\���sAs�;��9����f�h����������s����}
�\F�
N.������*��c+��dt��O���s;���i�[��.^�"���7Vi�_x ���V{'h�Wh�q�~Z������^���`��"\�
���8�l�M5��+��"CS�b� �!��^ju����1\�]�hc�yU\N���M��a�<7i�2@e{�j����`�-��LC��(w�o�<�{%���79��{O�{��A�t����t���P��EJ��Q��^e:����'��<Y���5�����j��=mh��n���N�Rv�z��^#�Nb��x�=
���,y�>�=���76&&��}��E�l��~����V��0�,m��:�>c����l����o��7���&X�i
�!�i�tNXZKW�&;z_����i=��^�����b��w��hD�J�Q����M���l�����yX����<��z��X�Ml;����<'�������DY�nm�|Ddh���h�����e�q(M����}����#f�Y���6J������7�K�P�v=$7�y,S+����X}��a����of���G������(�1����6���t��py���y5�T5�"
[^����%+nyZ�Y��N�������(MH�/~[��~�������d�oMs�b�"J9]��&�������}Nnz��2�V��3��y]!��U�'�V�R����]e��(Z����O�����E7Y��s��A�n"��69e������Ct(��v�So��=��8������E!�.�����)a`z��q\���(]�P=M��������7�}[I&�����^.C��5���<��r�(����u�t
,��jGo�Y{�4L�g�����AV=������V�R�V�76-<�9����R���Fd�O���i|��:�u2�7�;�������~!h����l�<��}������d�n�,�^x��t��x��uf���3;:��|��O���������x�)�b&���R+���Y���v�x�����g�����r�1�>z<
u�����S2�1������0y���<����,y���)��-�X2�������g:��hz�"�bWX���#!��n���%��6EXQ1[o�P����9lMP��6Og���;{nt���nk��7+r�.{k`��x0���]�������3D���1��W����3���=�
��F�����/b�I��/Q����cj�����[�1{��7C(3��j�qW:��1�f����Me�5P��8P�y����� ���CX2���|��~d����2��G������<�L#z���O)0+
��ie�/eY�e�-��W`�a	����v�k�oQ�w�I�����U}�^�tI��4���3G��>�8�%cC���Q5>��6��{5u|)h��#O�;t\��v�rn�:Y����m�Yo'i����2���8B����}��X7���8|l!d���}��GOY�q�dd�0���G��������,��QcTd��%�Bg�W.�1��o:�����0��k�9�}+t���%5�L=R��[�qKz���9�lB-�r��}p�Wg���j����C;<�8����3P�bJ���&F���������������X������FJ��������mX������{�F.|$O5��&�v#XN���/W56��=��P$m�iMa��=Z�O%�Ubr9�&3
���k��b�]w�
���u�E�����{���H�&���`���br�W�n-���#�mp7�v����Z;{���O=��Sq@�G��	e{�X	R���af���(Q;��,���??}��7�[/����ll>E�tVx�-#�;�����A(���d5��GO5P^���%
��[4���l������$�����g�������G�HC ���g]���HEwf������9�&y�q3�X��TW�>��<��t��v��U�(x�L;y��1��wg��+z��"���gb��5�f�H�=w��3��$��e ���	+6�����W�<�k�zo�����W#�I(qa��C�i��j����V��#�m1���ue�����x�5��d7��c�VY��7�c
��92�>���8�.�=K+���A��2���L����~��c�
����t��
�ZV�6���|���Y�d�W�Y��Zc�i�X���cx��s
�B�g��l7���h=��mV!�Y8���Z>���/���I��L*��:<m��H=��bM���k"�o�,�GM$��L��=u�5�^�z����0t,��K����u7��DUp����������=z�N���{����mc�}zr=7�<������~3����>�
z��<:�G��Fd����T����.�����S����^k2u����:'ZtC��o������:_��E��{w<��joK��Q~����"�����{>��w��/#H�{%�3;p�;�<>�a�Iv@�r�.�Q1w[�s���P^Y����j�y�y����"R�������D�����������s��q^{Z�g�Tu������������EUE���_m��oS�{��
���OOg*'x/#����|��E�u�Sq�U����b"�1�v������i�*�^�7��C���
}�;�n�||�{XV;\e��Ind_
�������������z���	b9�����!�;����I��
��k$5J�^E�-��������Y�n8��1��`���k>�=�ts0���_��������]���E�kN��_���9�=�<�o^��5��1nJ�� �]6J�C| 	�V���z�8�N��n�H9�9�j!�`n�����S��8m����zl�Y���;]�,���q�JO�8��m�D#��Qg��835�`�pNs:oP����7�IFd�`�k�z�v=�.��'�"��]w���L�T/Fs~�#R����'�
]x��2��Kvr�ao���������P��JW=h3@�����
��.��|�����T����0�Bv
\s
{lg0�w^���\,[�����!�8��K��[�y��C����	���^S��&��O,T!yp�K�;���6�K�c�����z����H����mu���x	�i������Q^i]��$yz���G�@*>�^�I�q:Jv��@�-����Z�������k�������-8�qw��e�j�9�v��.2k���]��F�Tt�4Twz	�&�������P2q�����\����D
��t������ia;�xk#��f��(��>�����g�X���
L�s���PUEQ3i����EQ�aU��}��%("�����/0��`�*#���g�����<�DE�Da�_3B�,"������-T���_v�+����pw	�2��zJ��jl�r��	][����2��i,����5���0��n�6�#_
@�����`�R��c���`U��o=��ge�lxFVE�]��f���;P���,C�xs�\}��TFV&��sf�%A��\�������y!EW�%�0�����.����M��/�XXDAZC���n��Afv1���F�������5C����o��}�l��@��x%��Z�B]
�3�f�V�BE�csygA�q�c;SD��RE'AW��\�v�o��rg�t(�(sj��6��"�,)
�u=�s��{���"(���g<���B"�*��]���*�,&�����dA��s=[U���v*(��y���7��cDq��\����7�L;��{�i�ovD�,���t�^�H�����$���cT��-��7��Q7��T/�p�qh�n������3�w2i��Q���Rn���jc��s����X�^s����6a[��e������*=g3��}�FPU_{���s�E��0���SU����9$�������UV��*
"�, ������n�
aQTETG�5������)&�6���F�NyR\����{^G�Y�=f���d�KYY��D]8�'���{��5���z|��m�A�%�J��L���t�C{KU���t.��[]��13=����md��@�����|�������Vw�]��E(������_{���,�_�Uy,0*�#\{Us,��*�!��v��k���
���0*#rW�1�*"�"�T�^^I����B'r��y�T�-�s����1�+:�:�L�;h:��S$�e[��r&��X�
�i7��&`5&��;.NR\����Qj��.�f�rx+�����AD�i1�&���(�d�'���O���W���#!��Ua��URy�<�R�QL
���������v!E�W�'��fg=Z�
� �Y9RN��j*0(��r�����XUR��v�
w�H���B��fk��`�o:)��[�N���{�b���Z]�\`���� �xm�-�.]Z��>��f����um���"'�V����V39--�������(|���-��$,*��/s<M��1\��\�|/�H�"�����9�	UU�X��]z���5D�`TQT�v=�0���+gcTEcfvI��an��v�r���B����x����pQ��Y���D���w�{{[�cD���_-&����
6@����\�d��QaM���7{��t2+��O�������}��m,XVDE�J�=������S���k�h����7X��(�X��wh�
��0���s���EXQaEJ�}�����!��O&Nz�EXADO0H�v�%.r=]���WOS�����'�mk]��Ve�%]oACOe��IG[���S�@�+��������Qc
��CF];�:����������c8-�h�u1�/��Yj�)US�r��)�FXTQ!�+;�9���","������P"�+�(��w��w{2+��-r��;���AQA����W5�{QQ��EOfv��Z+(���/�n����[M/����6��R#���O��U����,SM���n�,c^�����nR��Wr�F�p^�u�s��k�T�|y�� ������'x���'
�{�'s�2<������]	���TaE����r��B�(�#9��=���E@�* ��G�t��VAQE^�;%Y�3Z<�QTQ�d�nwf�U�*��3=U�%�Q�c�g�7���"�����D�K Q����e��������'u�]�[[���o�q��7������4��y@�!���� ��E4����n3�����nb�W�1�!��<��7u��
4^3K;�G�T(*����{�1�PQ�L����z�j(�*��[��m��e<�DT`a��jt��q�4@TQF%�y^��`�(� ��_L���u3p(�������U�	���e;7��������tX�V���{yc+���)CU)%gb��n�+d=2���T<��T"�]i(I��#����U^�.'Cr�,jNswE�^P���
�|�����#�Gb�vt��z�r�S��$9���}��Hc=����mUXV{�;Y�HH���+��Y�(�� �����=��XQVy���EVf��Q�N�����$�s#
���& `�AKvFw�P�����T(��|�X]O���l����q$]������;hM�9Q�A�E��r�(��n�5���V���+!�a����V^N�y�DEDV���{r�
0���>f.Qa�M=��ef��0(��L�����V3�������r�DQ������)�%�AE�`a�g�E��s�v�=z������u	%Y\��sFn6k��
0��"*��O��N�{4���yy{�������U�
(�*0�(��������g'9f�Y���W3�g�9����sDD�a
��z����0/r�[D������(*��{j��\����'x�{��'�;�}]��N��*+
0����}�zo3u�U��.����E_'�*q��� XPG�>����E�i�t�����IU����&�Ea��\�0�ew
�0k�W9;�nvx�6������(���PQ�
*����R/�����R���o{�Q�V�Xa3%+/b����[��
Vk�m;�Mr%��l}��a���f���[*�D3P_/��C{�� �����U�����}�����
�'V���P
�$����HPa��Os=s2H�����(��N���.z�daV!F]|��e�g;��
��%zs�O;:�����;��i�k���������<r�U9^�����*1
.79Fev�����*�*"0�O<g7���{�7gJ�����;�:ne���(�(����r���:k�t�����K!=u%��@UaDEU�g���Q�����u�������}��ne��
�+�
��1������,�3-���������qQPD�(_8&M�}N���q�r��(j�
�����w\�*�
��0�u���f��6�90j�r\���,R��.����a�RaQ������n:��(�,�5�\rf��`0XQEE@
��P�Y��i���3F�3����x���80��"��0����O9V{���_SO<n���=Q�
�� #�r���;���G��"-��k��P�
s��$�kd|&^�U��TF��@��i]�:���Ace�����V�����	y��m�I��9yy��7�8��f��9����8�(��
�Y��d�`Q��w�{+
 ���;�����`P�(�*����+-�XQ��������+�9�(�%�=�o�N�����r�h�QFS$��f{SS��*(� ��B�S<��5��h[�a�0�k��gUB�b�A���;��9���x��"�����n�U��QHP�g;�v�B���]]����3��*�`�>��]���EAUQEUD(�
k�tCVj�aW���v�v����Q�U�%U�a�QQ��>

���Z���3���v��r}���t�	�aEVg��v����>��Fe�����r�a��!TEX�����9[nfz{������x����u`AE!��!�v�}�{�7s���ki�w�o��3}'�XQ�DQ$[������������z}����e�^O���(������y�r�g	:���O*M�e��
��Lv\�����n�S���������R����{ce�v�bo�u-�0��VPCw��N��;J���9�3e� �Z������"���]���
*#=y���;����dM]�n,aQQ�{�u�����s4	�
��l���d#�,(������J�""17F=�4��EUF���Z������aHQ{$��=-Q`�����
�o0�p�������I�!�iX/�Wh�,0�"�0����u��o�#1��^��c��G)1&��*Z�(*���0��+�oF���(�r\�P�n�d;�)x:#�*0�0���g�����=������0R0���1
(���v58`h��b����}1.Uw�"�*�00���"z�����vEa�`��H��������;����FFT�6�p��'5���#�3�S=��w�����%EUa�3�}U�����:k|�-����,��1���0��-E����s��k8���ZwRUk+�_0",6����w�|������\v|e��x��U-�v7�
���A�h1��.���.��V��}�
*���ZNa��PD���
�n�H��H�T� ���ulun����hUW�T���~��O}e��(��������(�Q�TU����x��������!h)=��7����0�"����n�}��%�aV8�������J�*�
����P"-��(�0�����}���{K�����x�|=���*EEK�����7���������q��������s��������UT��p����]�0���X�Da��Dk�i�����wyu���'0�zi�6����*
*�@�$�RIog��m���4���.'<b�����"0�0���U�M�r���le�V	��f��(}�B��+�*����9��M����O������9Re]��VTZ\f���������y���$��������WJ����������A��U��mJ�Z��\����������
�
��,*��(�3w��H)�
|��7DgV��2"V6���C[$ �I�������sSd}.>��wf]Y����dZ����(%��-i�q�m�IR�iq������+6��}}l��]�VT9S���i�;&�*
*GfK��;��UF�����9����J$
�>���tEEUaS�'d�E����(��,*����7��S/���������9��w��� ���g�M4V_vtaVaQA9��K��n^yRio����0�syW��(�
����"��m������/�����r�����t���k��#���B(�1u6����6y^��\�_=����vu=5=1�aQaa�`U����W��}U��O����������1|�7]��������Ogz���UY�<l��Q�ETD����&���7������=�����6AXQEEE���n��w�Ux��w��d��s�d���.vf�`TQ�HEE�$���>����f��zR�t����|�f�*���"�����@���T��w���o�>��r��o�Q�al�����w�Wy��X���������3�lfj�1+I���X4D)!��/�Vg]A�.e�1���o
�����L,�s:��
��]����0��E$'5n,�/6�����(�_�,��j(�0*��R��f�TD�zj�6����0��/�K}�TX`F�:O>��y�B ������s����,*1�]e����.
���*����#��)L�����4�32��N���<�aab��4�������TSw���������A�� ����5x�q�L���S�`m��:�����*$ �+(�]{�x�G�_3�k����m<����Q�Q�\�����Z����s���yTj������
"���4������������M��\�E!UEEaaQw����6���+6}}��ye������}�QX�`�UB���{R*�A����4�r�"�s�k��k{��0(����sG������u����j�NS������*�,)�N6g+&_U$�:��)2*�KV	J|!�z1�j���PB��
�����k`L^��*M��Q�f3�*��+AYf��o�vc��E���mv4�[�@�
��#��l�N�DDFF�����=�Qh�
0"3��x�Z�,
���#O=1�r�����\�A�DUV��Oy�0,"�+sW�3���E�K9Ewf�*j��N;��$QVV@>����W2�
�%�Z��l<Ky�����Q!UUE��|v����c��%����Ip�3Y�wv&AHV�<��N�	sx��:�w}�58P�*�XU�`S8��._n��;���'������*;�����,
 �VPm��.�`��Z�e��2��^v��d��Va`E�E����o�y��^��
�+2�s<������ ��������"����:]����n<�gS7�v����VFVTT�A�����]i��5���e��x3���<�M{�1���TQQHA�D�2��]t�V�����/�;�z��K��x�VA���c����2�qj��}�s��*Yu#w*=�NR�������[n���H�{&F]������;��)K���sKt
�r'Sn�92�K���)�h����n��{]+L�N7����'���������0�"�s�g=��d�d""
�*, ����'Y�`�EQA�Z�����=���8���#�
,%��X��4j���"��
�
���}�r�'�,00������������w����"�*(�
�#�q�mnso=&��,�# �e��%L���q��S����y|Ma�J�[��b�����
����q|�CK������Eb;N�^q�U�U%��t����iV����� �����z��}�	�I�i����f2cq��3N�&����3�r�����_|��$>�+F�f�GbC���#�l�-JC09oL0���A�/_%!��a�
4n��;+':�;���F��mR"���n�C��4�8J$:$��[��M
�����jI��d
+6�&�jY�`o;��/v��}2�J��h�`�q!\L�W�����uNTrc�����f*���@:c�d�m���S��=���he�U����SK8��;��^�kA�5��:�� <�����v�Z
b��"�U�N��$�[���"��u��R�4��-n$���K�<�f��/�B�.��%bq�K#/��q<-3�����-�Df����c����K	
����qL[��2W]��v�Nnni�Wd��8��]n��S��62��&$5��(�2G$�="\Rqo,%�����7�2jb�c��?f$o;9,�jt�������|�,5�)U���0�s'7Z��:�l�P�7�iV>���<@���xv�h�L)gn�o�����%L���j���/;2fI���e^��f���6�t��^��Z�1��v�2�i�5�m���,�m5C�W���\��o61�����/!�i0�_�O%Z���jLNt}�b4V.�D��[`�#.|+8����������*�e������
��d��6%�HD��6U��p��Rjr�g� ��Ev��l�.m��3mt�f���6��U����<�.w���QR(vm�H�3,�t\h1L'K+��=���ce1�A��J��
t�����`������'G��2�4FSH�B���5XGj��0�jn�g3\�k�x���{-����R�$�WXw�-�fi�k�	����*t��x��&hc����]���U�J�:�_>p���.Q/:(�J��v�>�I��wj�9�\�U�Lq�-�.e��]���q�w���	U�Zdvk�j�����Pj;YC��Z*��U�������R|
4� MCd�oh�z��EY�}�2A��d�5����Xru��
K���B��ss����e�a
��j��e�&�h�#�����N��/o;�}3�\9t�+L�0�����5H�-*$�28PG�S(���2^4���;$�����Z0|M/F#x2����3��O+u���P��G��9{��R��of���pn�:<�Wpl�n�c��5����=mowF���\�%<��pJ�SN��k�\e#�g,u�C���uX�B�bn^����.����-L����@�tf�L�V���Cn���l��#�R�����S��f�
X�0��R`�����F����F����3�m��1��jd����BH[�|!K��RT��)1Ug�2�k��m���{��c�E��gP�0���Y�r
��3D9RUS7���"�-���j���7�Oh�hK��p��D���4V��i=��b��IaRI�wg}{;�b�J���}u.5x��<kg}�d�k��x(�Q�]��(u��s�Z�;�����p'L�2��[J�=���RK]���NfZ��\LO����Xs6G��.H:Q�N�/+6��/���4�,�4�+����q����M�����������e������"�-�M��.Efd��5�����(��I�B�2�+2��`�o���n��-�u�4"�"���_�=�������6��d�%�?u�)�5��'h���������]9�
��v
j��vq3(�9�)��{���lo^����D���u���y	gGdF����P7]�����Z������fe_W
�+����������!�5J3/zF!B[�j�T-��#�M��O�O�+�uoS���8��l����w�����u������5��hmJ&<rMb���n��V��jr��a�������2@+D�#mr�A�w�p�����G���"�0�hhQ>7�����s�
��W�>�����}�z���@��(6�-����m*���!t4I�oe�g-PV�����&�q�\��|�y-��kqV���7��r3��tk7���V��*8��].���{�R��&UM4
���|�V��>����L0���Q��v�Syrf#Vf�|�����*��usF��%��g��Wu�����}v�(]WP������/2����*�U}�cG�	�~���tbbn��Jx�����Xb'��y
��j;����<<�t�r�}_S2�3����r���������-��}��P���x���+���Mp�;�y>��N�#o�hU�D����Z#�H�
�T0���o�-u��|���F^%�{�v�X�@'��l�\$%vud�-�j���[)]���:����39K
���d:oU�:�����t��)�e��dY<b�u�;70���j�8K#��w|�_l���t�3�V��z9��!V2����]f$i�s]�{�3�R\�U�'`����	,���6B��f0�mE�&f���T�&��m�a�M�M��(�m��Sf]b�ig��������b�4V�yrR�^��T���4l��I�[
'M6n�m���4z��Ay�gl{�����x*�A�Z�M���;�T�)�a�34&�6�4�d$��)��(�����Q����)�u*aC5�B�X���M�#5�sl�'u�[��d��I��6�l�%�Ru�`��C��^�#����ky��A}W��f�e)E���,`�`����SA6�n�n�l�4�;]e�m�-o���dx���M>7-c��,.R�K�i��N�����F�"�
-��A�l�	0�d4�-������f�FNv-^uI���������Q���Mk����Y�e�	6�td�-�S�����77n������Zv��EY��}��e)WZ���iH>����=����AM��E:2�m�i����X��ml�e�C�h�����g��sP�V^f� ��Z�Xj\��m���E��l��m�I�[���\���A����^>��j��hf=|j��3#��Ef&l��%��e�$�m�M��e���6�f����tY���C�����H_0���*�X��@V�4-���:5��v�M���#��b;e��gm���y}����&�es��Z���DFv������\���GF�8�d�5���t�A����'F
��V���{
h���U��R���l�l�A�c��0;��1����G�d�Bl�GwUr�+\��4J�������=��d������}4b�L������s������6����+{;��4������V��\Y��,��*�b�V��j'��	�BR��A	��,�:�%B�<����0����}7����8.�`@WN���o����vH�op�(��fZ+�k�B��w�y�-���=G9�k��E�V������D��X^+��3qK��.v���+nR��p�S\W!�\�e(���[t���.�sn��6S%��m��i�%��i��i����uz������om.��tM�=���/�C�iv1��������z�v�N�M2�$���4��m9�U�.(�wt�=��m���/�m���\k�Z���`��hwI�Se�N�m��4�m��s6
���[�y%
�z��c"�WYF�H"V���eF[d6�)�[-��m6�2�^u��q��d-�'K����)�g��en��.h#-��l�	�)��`��-��A&�O����r�WU����s�!1������$j-���9(��-�^$,��vl4�)4Za��)�-��E��TV}��r
����@X���%\��-U�h�zmf�Y��/h�M��A6�M��m6�i������QI�X��
���8��G'����~x[1au/Y���l��M��-�[M���|��y��mKr��sn1�w�����a}�! ��-,��r���tKt`6�m�������[�/��d)q��:d\�#����U����\��\���N^�=j�$r�e�)b	��h����m���[!�O������KX\�nv0���CQ�U.�q��Y�X�`�V)���F���QO���v�1���`vz*4�r���>Kk�s�V��
k�X�Z����"���ki^!H��VAA������:,�c������0X���}Q��������%$`��v����.�uJ&�����[�Ht�#�8���M�NmM��v�.��������J�B�����"3B�_wj5;��s7���G�}����)p��6^���U���e�����8���v*�
���r�r|
�������e+����e
���}6l���������nl�+,�UHl�K�R�[�� �+�y���u�[��7���O��m0�����l�4�'Y]w7`��|m#cZrs}&���������
�8I�M�]g.���v���0<u�3:�O���	XW^�������k��phq����2��*Iq�m�SM�n�L��6�t�pb����f�]����3��)8rV����*�y���uy�Eo�&�
��i��m�S�L��n�l�e_ ��er�������`Pk�1/�n�`�+pN�1Q��[su�2�4�e4h��M�[%&6���G�$���7��<�y���3��CZ���p��[�J]�\rP��i2�a��I��-��l�}�UmZ�[S	w��7S8�K��Dm;7��,@Tsz6k�������[m��i��a�L��	�Rc���gV_f�W��C.o�m7��J�WG�-�
���aF���	4�m4�I�[
�[l��%�{���>��y��,��|WW(�X��� ��}���pK��@�]�-0�d��)�i�i��,��oLp�����>��;�vZyLj��T%�I�ZI���q���]��
i6�m���SE&�M���;�j[������>)r�������\��]9��f����&5�3KI�S�Sa��M�e��`$����]�u��������sn����M������2C��L��6,�Km&�-����L��)��ya7o��_H7�`e�����hL�[C�-��w2M�`��M�r1���G�atu�fd{����4����q%n�v
�i�� U�j��b�Rb����9���+���L����7��6Y�t������*��%*[!����4#�lUk��;���WJ�J�}���r7������b��7�
P�5�p�)s���.�#;;�y]��VE��M��=�"�����-��\��C����A��1�b�������D��[8cI�����L���D�k�����R`6��n��7R���Z�wW$}�:�'�1�m��W��������]Vn)0%,���k�2T�������o�����`Q+9�Vn�lS��w#��
7�Rv!��+b�+GdGzu�T�6r�V��Y��\tN	4��w�������J��l�=��r�B����gQ/Uj8R��q���M��m�t
a4�-��a�/n������oLk����bc��-u���^^*�j1�!�fJ�����m��,�M�Bn�m��m����n�wgDS�������A���I�i��v��j���f�k
)�[�S,�I�L�m�%3YMMO:������r
#`�:�^�-������3����z�!���k��M�M���2�&�i4�f���'{��j����J�2���Br�����P�%��IOm��"�
�Rm&�i�SH��x�Q.�Y�v�,�L�����]]M�iG����$N��y�$�-����A��l��
2j�����y]QEp��;��D�����3�5���V��o+5)�K���m��m��l��e0�t�e����<�Sv8J�l��|����h��g�
�v�t(��&�\�J9�3M��Zt�tKh��)��a�m��fo���y�r��#�v�Tr73]��!��E�)��lj+J���	0�l��I���SE6��f��T�aq�����F��
��pE>������Zh���SM�E�d6�m�m��!�e��.��xw)t}��m+���d�����ql���M�yp�fa��;oI����R����r�M���:�a�;��0��vk������/W'��+vg���h���j������r�m���'����$�#qM���S���	qC������n��O�]^�z��74�t*�����7���.t��������sz�
��|O'};BF����
yI���=�f�+�{*0��c1�fq�pb����a���XT���mkl�>��8[|���.j�������X�q���fPBC[`�][f�6T����l�����mhUe5�6�>��o��@k�����-�nnS#(eX��O����\EbsLf����Ca��pb�.6�dlN�.t�H8�l=�{�^�b.�e�| �W�i��R��#�]Q=�E�����3-�x
l�I��%&YM���4�WjM
CI���_
sY	��AXt��e.���a+��m��i���CN�M2�I1�e�y�uv�l������I.6�PJ�*�B��D�!��3!�x3D���M��)��m���M�m�=u�.n�����p�����Y���4e��� �B����q�k&\)�D�	L"�	��e��-��K�s��_`i������yY�V�
j�e��w�Q��/�� X��3KL��m2�l���I��h�%�m �
U �nSS�h�\�p\�����`��u��c%��L�E�
h��I�[M������fZ����I���F\q����\lh��[���{.k3V,��m��m�Se�H�M�I�w/w���,�|^��������v-��.�
���Gp��"�&�n�m��6Sh6�)��gw�\��M����,�#x�nWV(�nc�x��
kP��M����n�N�L2�
:M�
l�m�K9w�@�k����-[����%Y��w,l�A���e�4N��ri�Q���`&�I�S����n����]����r�.�PG����w��W�m�em�J�*^hv,i��]7,��K�T��v�)����u��CYy��lN���^%s����]X8X��1^�6�E�\��W��x��is��`�jMH��4n�SF*�����'XZ�m�1
<GR���/�.-8j��%�����}3I�^�h�pj��U���u�X���P�l����_UR:��W�Gu	h����W��k����<u�?�H����A|$��'v7�6.���3o#����[;�OV�a���b�I:�[Ah�X��N�~�8d���G���q�K���Cb�2tY�����7���P�r��1���4
����W=P��ovG,M���N���l���QTY��p{al�[�su����5�6��c���8V�Ydw�d!v�+�Xb���:lN���Y�pB�=� %��L6�6�-�Zt�a��i���#�y������o��
���5��=�e!�B�&������P��	�
d��M�-�m�n�m����W�l�u��y���X&[Qdv�mM_6�o�\�[t�d��I�[a'M&	�ov>z,d��59�V�>;�p���v�&�T	��]�i��S�[D�E��L"�I0Zl��^uv��6t�X��w�Y�upe_|��?v�3,��l��E�,�KN�l��d��I�[��L�S��z�o�`�����v���6'����n�
��l��)0Rh�m���U�@�/�����YA��u�E]�i�f�<.�WGZK�W�f]noPi�[��)�L��E:)6�g2��2wsv�R��]Y|W���b��v4Q�b����p��I2Kh��)�Sm�H�Cn�O�f� �����9���X�^,F�m�Kt�%]�T��Z�����L��"�%��Ktd6�m�S�Mg�����0�����^��t��|lu���v�\�J��d���I��%&�m��I�Jt�k'�gb��pb6�`I�C@X��K��b9Lm�r�*�a�77>r�9�t,A�j���u��*d�Kz���[y{�m=���`k�u���2��!\���vTT+��iD�N:)5�e������v���t��M��u_�����z
32Z$M����d��_>��\6D�..0O�Z�c"n*0��:r������;�����������;��ba}�xG;�������u���.�Y��\.��:4���X��jsSc��fi�x,1�t�T<�WN��YB��W�=:��|��'�S�U��f�;��j:�o���@��b�� �5�TQhg�;�q�c��,�`U�iX�R�B
�B�Jri)=
�iT�#.Z��tz�*a��x�oD7����/q��{�z��������@��1`�^��������\�Rwv���.S���-���*�����)�Af[�N�e��e��I������Mf�����cz;0�4���z��C�:�~@���.��r��8�L�%��a��-��,�Nb�H]�Y�JO���������d������6�6��m�2�]	�k-4tZi��M�M�[h��n!����-���b�����bG�C����Yh���(���s1*:�ZZN�N�n�M�%:%:M��u�y<�����2������k��TK�������.�f&*6\��2����M&�	�tSh4�I�����;��*+�7s������2��*��V��/�}�6CHK�KfD�M�h��m��H��l���SC��L�����c}�X���z�rE�x���V�-�
Z�fRI�I��D�YM��	��t�ys<z������Q�k���������2�����������������xKh�i�[�[�S�@��M��|��c}���i��!�.�1����iu]Z�U�Z51(���������)6�m�I�K`4�M�[���/
KF��V����J.�:�/wVyS<e�b��A��w��m4�m���Yn�m���l����3�F`�-�c�����h��R�9�_>��S4����z�Se���]�]�X;�m��}�Dcnq�4�g
����$�	6e�Mbe5����W�9���2w��s�����d��5*G1s��l���b�
���z�4����G�v
n=o�.���/b��N"���l��2�Z�Vh�XB�d��3�Ry�b�]k�x�%O�sj��m��_f�0�+��R���+z$��sY�z�kY�/$��T�Pj���%]��v���
�U0�c[�7+�a��S�_n��x�x"
5�-��{�r7��+X��V�+�9S�}}�TUp�lW�I�)�����9[d�Y������{j:�M8Cav�Wo���:P�v_GD]���:D���q���{�����-������{/31&�5z+ a���\�FWN���k�U� ���d��Ak�h��e�Y����a&�$�&�i�[	��N�m��������]����%�C������.�l��E��2`-6�m&�I��	�E��l6����f����2�nD.fcQs*�[�5V����$7/S�HN�4���6�l��E�	:E�)2�t-�G�;��r��������[���{��N������8lY�j�JX���I��E�JL6�E:)��m��v��k��CrB��X����A5��i�[7��-����3f6S,�M6
L6�M0Sl��M�u�V�
�����[R��n�|����fR�����3u�N�i�S���[-�RN�n�n�OFwmv�6���9�Fd8���������$/�wE�_����
�Se$�i���SI7@�M+kpQ����e�y�*��ed�'�`\�7'dM*EI�x]���c
:e4�l��m��
�L����:NJ�l��v�N���Y��������iQ�w
�uL�"	:)4�e��m��	��L$��n�EK�F���up�Wc��=g���$�����(+�9��4���
m�S,��n�L���
}]���v�t�m�����;:�.�rt���@�)���f�cy{�4�$�����pA`������'�t��#~��{�xw
6�c��1aW�	��oI`^���hC���9�b�u�������i�
��v�%�u�G(^J����F�6�W��6
x@��}�dSp�u���i���k�Mp�.fZ��L��0�l��%nLa���#�y����f��k���
i����D�Z�6��H���T���fu=����/z��,���
KWR�j�=Pg
��[���G�X�
6;��@���5���v^`c�����I���l��2�!�[����������R`�����`�3���������W-
��S��c��Cq&��}�����1���"5���Y��&�����8���>S9�	����}e�M��r3�iZ&���mp��-��e:
C&jrA�J�+�L(�@�l��6Sd�i�[=;���������wl��������XJ�v�@�4v�������%:M�Ze&�I�SD��O�	 �#��Ev�au���`��4B�wm�����*k
]K�����HD��l&�%4�h�)��`�[m@d-^����9c�*�m9��Sv���
PFZ�2��ZM���6�d��m��,��
�v���@����YI��\��-e��e��T���[��MQ�%�S!�N�l�e��m��{�7���j�7;I�R;x�=�<�������j6&�e��3+��t���St�i��i��i��l�e2d�]��f7��:���}��$;�b��p�P��X�'�k� ���r��d��M���)2R`��m�����S���-)����=Z��K�E�zE�+�0~4p�W�MJgv��,�N�N�m��i2�`��M�\�y���\��,J�d��(�2f@�o�=Y��������;�����1�[�S��I�L4�i4�t�`�����1[�5��)
1�w�!������j��Uf[�V�����
6
d6�I�[-�M�����
i�n�m�c��G���[��w_S��mFJ���.�FSiTZv��N��N��E�b	�Q36V�%`�"�DNK	�]���v�����f�������Gf��-:�'z���rK������5�����Vk�aTs�F�*c��%.����9yN�H\��$��]�������r��>hp���V���Hrlt4E����C���z�#xDzv���!�����@��!7;r`2�^D�qrDb���zX}�����t!��6��c-a���7k,X1��euk��(Y�������������p�#Wo�5�.�(�g�+WS��Pv	�\�����U�9f=�����u��V���
J��)f�(I��H�d�w6�AK�V��Mv���"�<����[�y	Gz�:M���=u���:u���R�b��hD��r�Y-�Sa�L���%�e4K{,^���N���Hv��:���C]�c/�WQjsn�)�[	6l��e6Zd�����n��-gs|G\���A��>��`Cv5������V�<G�w1��-�I��L���&�i����3fJ2��$��ly�y�*
��4�����U���F��1�dn�7[m:E0�e��I�S����_M���2M����/t���31<���yJ|x���U�q����m�tSi&���m�-���R������hf�l���*���s[z��S�������/!Ki��a&SN�l�)2�tJzy������^�N������P���H��LS��e���106��4�a$�I��d�m��)������-�s����9u��X���;�*]yvx�m����6�)�[�M��L��%��`&�o.��[V�S�(te.��9�L���T�WT�u�sS2�����[�Sa�ZM�
�m&�)�Y���@a�������+r���7��Yc����k@��^$�ke����`��)�S(��N�l�4���L+k{?���S�{4��f����]�c�t�B��v�2;E)v�A���m��k����,�^"���at�K��zY��
en*=�zo�w��Q��P��^�g�|Ea��9�e�.�	
�("�����["��)\��}��UQay�v���W<TXQF�����g�;,
" ����>��v�>�*����7���f���S��[��<�~�)���|@On^�D���������\~*�,�
��c��_m7�������]0�"�	�|d0"�(�w�+>�%Pa�N�y���XA?W.��h�19�~�	�CEDXyu;���d�H��,*����foa(DXa�����9=�y�V!��ewbb������,((�(�(UoR��u��
�XO�G�:#�Q�j5O-{C7fT�����4�y��P)Y]�Y��}1#O�[�V�����]`�L\�}��>gi_����������2x�;9����"�"���f����2�*��>�/&���t�`U�H����n�M����Z��0"��*�w�M}�u1EQA���z���!a�������}�""�"�*�z��������Dj�H���u����)����+JNeOU��&8v���/5%�%{_��-���0x8u���t1�%uK�J�q�{9SU����j��Mbz�:������4mc�X�U�R�y��s��~�P�3�'�l��+,G���n�(���(,"1��}M^���(����������x,""�������
�0�rNwe�s�����TV����TUVX�aS:S�V��~}7�p:Tj_���2���Q�����wf��J5g�[+=�R�y l�dmyi���E9����
Z�n����,��;v��-�h�N�<����<�R��\���
0��v��}��s���,����o[w�����TV�vy�����`�+0�
����J�
@�1�v����V0���]����'�gUVEG��������a�+���
_�aw����l��<��X�	�&��#Gn[��r������<H�9�|�~�n��D4�k���5������bF�J�3s�C|/�Cv�&�Y�)�a�b�/�Na���[I����8���;V����QEHj�~��{q�DQX;����=���
�
%����sr��FEU����h��B����+-U�M����HQN�7�K��d�*"(��eG=@��a"�N��Z���U��e��7^����(j�2��w^wX���)��O_���*�d���g���Z����\
6��l���������z�,�@��u
%�X$�2b
�{|�X��P�a`\f�����QEQS�����vEaQ`���w���;,"��"9�W}��f:|���(�(������_����a������j!�TTL�w�s�6�s��"(�0���|R��6�O#������j����w3�0�"�s
��z��{�E��R�D�/z��K�/{.�6�m�����^��r����of�!���d;�#b�+������*n^�k�X�T�MrodDXX�W��}��k�<����
��+���"�0(�z\M��oHXE�+wn�tXPU!��V;���o�����p����=�th���AUEEQZ���g$8�����x��f����X{�"�T�r��,��c�����`�X��*�������OIK#r_���U�k��n����B�:�&.a��K4��U����H��%����e0�/�
����N��pQabyu�����QXaav����g����
("��{5����"*��WX�n^���EPS9>��QEA���g7�����UQQ�RY�i�@���+��������Zy�3��:Ey�)��V5����i�/w�������
^�k����hPQLx�izn����T��;O��n^_]uo�L���g����ro��uhO ����w���UP^���;m0�
�"�"�������z���a��}���n�K��25TD��g96�x`��0���������N��E7}��<���J
�*��(m_���wiz��VL���I����������T�,��9N\�M$'�f�� ��RE�����@z
H0=^�|(�����'�������7��]���&r�����gG�@��#
'�s����a�EaQos7~{2�QEGFr��k���_o������
"���}\��5^9(����r������9@� ����}�gK���b�7�d(0�(���}�`V������4�;�b(������b�������a��<���w�OY`����m^���A������������wex��[Q�����}�X�����������C�
��.��x����!�k���a��"���59Ss?r��EFXO}I���qHa�P�/����6B#��N��g����
���U����aO}��B��)_M�|�YUaVD���Q�{������v��).�#����}��=�����[�����[���)��S��u�!b����B�W�����NN�-�8�;����+ ���c�����W��k>�s��7!DHD~�;5;U3��`aQ�T��V"*����y��ou��Q��0�9C
"�
������*��W&���*�(�z��f�}=��**���n8���=w����w&���bs��Zw����}B���
"��������I�A�f<��F��k�8��eQAa�������9��:Y����p���1Ea�aUUVQ�M���{){�{�����Q��&�����������t�u6\s����Iy9�q
;�Kr�c2��00�
�0�;�]^NdW}Zw/���e�s'�u�]��*���0/�W��-�Lu}���1t[�v�
��/�>AQ�XDEG��;}J�������5�!��8.f����E@�"��B�0,>��{��=�������S�w�����FaXX����sC �u����$IN��2S��Y<�r���^����ueMO
�z�.��v��=�W���.Z�M�~���N�]��\����w����<9���o}��b*��ayE��(��#��wn�^gEUE����yu��
�(�����5�^����"���9����b�0�����N��
���g|mb:R�(��o+�V�.��j����QaE`D���5^��]��E�_���
�t��B�B�*�T�
�{����R���7��xt��|���O���
�XV�U#�z���VI�[�w��{}����V�"+
��L�*l����nKy|���sj��(�U�VA�5�s0�r�����O3���ne�b�EQQ�UW#y�_1�}~}��{'����s��8^{��C������+F�:�_C����^�[Y����J�5���	�FDTa�����EXJ[+�9P7�+E�B�(��"����Ys�Mwj{������Nw�����)�"0��#T�-%��1=�[���O�{����������;��I��@�����;�b�u8�5���-�o&����:�K_y�J��pw���������^�0kg�:�mn��s'���}9p��(�o
�y�gt��x������Y�0��1
�.����<�,,
)�w���&�' ���
,9\����(�*0�	���6��
0"=�
\���U]c���QTHXUQaE,z��Mk�)11����L�&���UAUAXV{�=��}y���Ov�]�{�wwFo�!�EY�Q�s3��y�
[���i���B���*)
�<�v���s�s��7�]�z3{\��{���9�z��*�(��,5B��s,��OT��I����|�N�M�����, �sI75�f����Nd���W1���*(�
(��,��^�W}9e���������9|�fw�"+B$,
�{d�Wn]=����v���9����QQ�UQXXY�{
�Vgv���{4����7'�����f8�
(��0�|C�6�i�Z�����sp�BM{� ���G�p}�]c]�X��X����r�n�3�x}*.��D����$7@7XKJ�|�NR��
�c���[���{���qP�,#���|����y���("�0�0""�������U�����*}�_�����XUa���]|>�A�Ua$�k���J,"�� �+
����v�EPUYk~���{��+�(*�$��mL|��I,B��( ��*+��/��;�u���/����5���wLUFV;�x�{��/����e�{�'������M�r�EPEDr��;�����2�����zk������w�rX������*�U�*�����dm�����&���!��TUUTU�2��NI<���oWV���&�Y�����`DXTQ�EX`X�&���zv����s��uu��/;]�k���j�
�"�,0� �������;�Q�d��������|��f�D�QXDU	���\��zS���J3k9��s�F�}���@QQX�C���2o����]�-)9�������;:��(��0��*"�,%����Yk����dqj�U���e;�_�n��]+������F��K����U��M��q���[J�^���	Q�u���ms5�"2�c�����;���}1����
+�9�g�{���F`E�<�6���^�����o��F�XEb�����`�*�q�U�������FQ�X�3^�u�
�w���Q�_�R�;���d��d��
�"*�
��*�r���_w7��1�/��{�����3�S���a������t2���7u����+��1U�T�������}���*�*�s���2y>����o{��#
�
�7=;���op������j��������0b�"��������L��'B�Z8��nk;}6Y���TTUTaF|R����0�q�fgdY�z���w����QAFQT��n���WV;I�a�7�4���@B�EDFDXai��}�=���L�W^<���n�jf���t�E�aFXD��s�����g���8r��s�>�r�y��$"���$�����
��O���]�X�AB�����l�g����G��a��7�����z����w�7�{`�D��$*���	�rQ'9�I		��
���b�9���s���
�
[�����,DR;����F�"����0�W����o���UT_���r8*�
��g'���UHAze�.TC

 ��K�|k�*�*���-����0���� ����W����}k��N�Ei�������DD�EU�h`�j��r�u���kt�`K��L7����"��"��	]3�{����������snP�<V����EaDA�EE���j���9���(NY��-���R���Df�W�QQ!UU6��sZ��r9��n]��k������f<�ex�� ��B�>
U�zD������6�A�m�T"
���"+0(����=J.�9����63>��,%@c�<+��*�
"�i���xNz��W�,���c�[��q�TDa�EEU
��Z�$1��g[�k;je4�"s�{T�""� �
)�e�!^F����������d�8���`�-���N�X��U��07d��y�=�^q���e�4�,����>�^�H�z����h��@��ZM�L����d�2�W�3�w���+�@�of���f�����EUA��3+v5QUUUDk���g~����,B�"�3G��vy�����,8�m#� G�D[`��(���t�dVFQ��k���U3^���*APaUAXGw����LE��q��,���n��}��aV'��<=�|D�{������/xV�xTaDE�cw�/��/n��q/{u���d��� ��������%��	E���������]Z���MGXX�PaUQo�����4����sQv��#���Ow��N�0���")���|ow(���������{;�[5�<(�*�*�|�S�L4#�m���������QDQ�EXaa�W���]<���V�GN����f�tXFXaUEF\�9�����g2��|Ur���53|�<P@X��U�wN��G�TP�-=P�E���$kty�{�P+�1�'|{N���H>F������W����]�`7����������<��>Mx��0j��i��&z2������APV*w3�z,C�w�f����#
��	�fN}���L�XU�N�������!�Fa>�W��s�OMaUUQTDQU��rL�VE����o���**�"*�c�IwQ�K��
�((�
,*��m���]z5��~T�od����|*��"*��{�X�t��w~��rG
����sY�������"1
������+Y
s�X���X:f��u@*�*�������rM�nk8�������9���m�shTUD��
��U����J�
\MWU�of��������"(��",,x�x���;����p�_Lb������pQAQQPc����zr�����_��������x�E���0r� ��^f��<�
>\"@�)� �{\��������{2�6\����UsK��k[�xtXUQ�`U�Qt��<��}�s��^wggEk�Mop�;K�
�q�]������}��M�wgW��s������\�e>K���/U#�/�J}t���O��������m�G�P!q���u�+�����=�w+g���~��u�$��TE�XF���w�m����
(�*��Q[����*0�**����^���#�� ��
(�������3^W��UtTDE�0����J}�TTaaa�/�oy��=\��""00��**	�����PAEDA��Ea�v��y����[{T���^*
��c|eu�!���&�,�7^(w��
`���;B�iM]&d|%d����9n�����xa������)�8���:W>�{Z�I�R�r8�N� �T
QWd&F�J������Dmx�X3,�����P�xp�)r���h�g)��s*0�<&���A,��������"*N���qw���R���C���~����8s!�;�(��*i@�+�l���]���X�����|��}X�4��p������d��w9v:��^�)��3�4��{L��/2a[�����V�C�#����_������&�a#rrP�^f#��uZL���k���C{y�c�|�y`���J��0�;v0�R��^�m���[�S�����[5�����3�^�������b	��[�S�#X7���j�
Pt����s��Gi��`+������]l3����f)7�
�9y(�]g����YW���8.��)�s&f�M�=X���]����������K{�:E( ��U|��E-���!���W
��4r���bgE�)SEGo/�A5�g�o��8�}�.��N@WZ�w�q�*��,��4���<�X�]>=Yn�����$�
���}W	b�n_s&0/�vm�V3��C$fuk7x(k���;��r4�>���s��y,��%P�n�GX���<�icU�
�t�{���u�H� ��%.>+'t������f�h=X������u�D��j3��Gb��/:q�p���{Ik��sb��+�j���KQ��])�$z����0����9��4Z4oy��q�\n@�s�WX�Gr���4�04���#D����M�v��}a.��I
�X���fgR��W)S-o	�][6�Ks]����],���0C9fL'��bxF@��T�Q������y�{�eHW�`����W+y�\�,����F,��'p������`�v){f&�a]*�T}���7��^��L66�6�N�_p�{M�W������G0#�p���>�p�vM��&N9��1����5��EWd���#��PI���^�r�Xt����
-R�e�{nJ{����U�������<m�]������/A�y�C��'+s�4��l�����"��M��W��Rr�y�U��V�,G��k�g���V� h�Cf>�\K�he�Kd0�)�Q������]K�l�.
��#����6�f�e�������t��^��
��wb���;]�j��v�k����tcnaB���
���zp	Y��n'���D%i��kMA<��Z7N�!V���*^&zz�q�8
����n���U�+1�N���S4D�,,�H���gE3�76���=X��*PZ�|w'&����]����q�i�����bc��E#�k�k]}��`c����3�Wuu���i��I�*i�G��k��]�{]I��]����`�y��D�j�p��&����r�f��'a�Gh�E4vZ���^�N��������i�Z�
k�D�,uM����Uf7#4kSS�k��:���v3�P���R�Y��	V
������p���V<�9�,Uu���5F�E�w<,���we���]�|��W��Y{�O5
*�_����s[���E[Gd�S����X'`V#�C)�%��Mt���P������c���/T��U��Ky��2��k�uV����4��Y�dQ/�ts�i
�}�k0�����$��<�)���;
7eV�AwujeL���Z����#�-c";��{BuW�����������q��8p��tWM].���t-N��1v2>�-V
7d=��^����PH�c[�����+�M�pk��;������������gs�-n�V���qaB��������������i����	
5���v={���0Ca�#��{��\�;�h��5�u�9���/�!���!���!��-;����;��;��A�C�;��H<J���l�v��/�,� ����s��Y�q�	c��
Vqh6����c��\�xi���ysB�\!4�*��J�Dx{w���Xf�p'������Z�}���T�[��,���X ��d;���]Oj`��">`��1��:�@���k������y%z@���� �}@jD[�f"7����kR,DW&�����QL@G�+�[��=�1�y� �w�Q�}s`5_'�K@+&11�rb/�k�����n{�X'��#�������0���j
|��������=�Db#�G�@y��C(��>H�dS�,�9��hF'�1��'������*A��r����'9Q�@<��9�;DDx����{�D;�8�vc�=�W�
Bf+�DSs�#�U[�+���<������q��#�������Dcsd ��w�>��!(u�:�q�>�6�5 �"5�X�2���099��|���nb-"1���1��%�'_�O_rHD�"H�/�H� ;2������g�Q�=�"%" ���b���s��dG�z��&�H.yQ�A��!`�""#z���O�����"�H3f� ���@�>�����M8��K����Os�Kk��`%�GX�>cR�����8�=N��G{1_d"%���!�TML�B�>���������MT����o`�M�*�������=!�M4m��H1P��4�G�uP�+�3���dV��O=�fhd�_
E�_k��7F���:K�Z���.�O4e��h:��j�1�����b����{�,����2|wE�H���9*�7�������n������
��Ss�PM��3h��h���$U�(�!S;���k0�hX�9'z72J�U����!�"�z�N�k��Ry��� ����`
c*O�0w�PD��	����7�|=�����&<������� ������\���){z�;�� wuS�T=yS�������;��=�X��a�~�{����_�8�Q��E��s�����K��s��w`U��d���_>�Q�>�9>�9(�#
�`�V�R{��x������������z�
����+�{���G$�_1�����!{��������_z���Xu��z�H�V5g��'���W�{5����m\f��R���[I��#�eqg�����i�D�O�?��>����O:�V���Xsj��uX�����a�_}�����s���f���7�<���u��s�b����rcN���-�-���:I�����T�������=�H�����mM}6�ZG�uu[��+&R�>���~����-:f��v5�d���L�}I�y����9�O/U'��}���d�<�{6���|���5�{��t���o��fL�KE���-7�|�RwuZ���j��T��gw+���Ud��l���e�k(@:���=(b��jN����'�������Q������mB�Vy�U��V����I��gr�;�U�R��?|����y��/U�����:��%��w�u/�*�@��O+��������eU���I���' {�V5T+UZ��yY<UI���~�������}�;��V���o��f������;&1G>$f=����-Y��PmU�Y+���T<�����7@��^��'�1OP���8�`�`Z`���Q��>����u��`�9,�F!��H��{�X���Z@s����y����#��T�z������ u���Wv� ������,s�����}Q�D�(F1�A�"��b=�����F/V!���@
I�2�G�1]��!�U"nN��A�vH�H�1�")�������-���`�1RDq���H`#0���=H���^0R|�X�`\�GU- ���Cr����>a7������5�)�1�>��!�G9���;������fj@)�7�(�$A�L��H�=�}L���u7o;{�P���H�R�~`1��y������I������rx�Gf��b�����g�1��0)��� ��DB<����DVO.B����y:�w�S��#�� :��GX �%7�lS�T��j��SHA��H"��y�Zs�=N����RX��Sq���_�SR�!�I(A�#�$�$G9 yQ!�'�zO��%������f"�DGX����b���� �#��N$q�y�\��51���"!b�A���<�xxsda����S�M^�D�
�Q���1
|�?w,��<�<k�cw�6����]hW�gh/�8�F�5�tJ\QT��;OX��������;�����+e�3E����v!$��%z���^��]W]������!�{[5�E�[�}��vd�dpw+M�x�7�+U�<��U�-�
/�](��Z�:y�5�����e��l6������#��e�xms[K0�k���ttU��{DX
�����V�����#�W�z��yY<�d��NJ������A�C���s���N
UPk�}�+\����c�q\N[�|�w+y����@�U����'%SI����������P]+g��lu�ru�6C�a���[z�D����i����I�Q��i��J�<�Tm������'�U
���������L�����5�(��{��F�N�+x��	Y��c+�C�j���������C���y�19>�T���n�ug���S�,t�o[�n�Oulc]���uo�C�����q&����O�nJ����.���`x�O^�'{���|������N�?��o���V�`�xx��mGTR���}q/���X#���1�8B���*O�UE���j�j�����n� MO����.���b2��`5��v����|�Y���{�T%����u��Z�����uj�z�'6��V�U�mYkVU�+j�O�~����U��s����
W|r�Wc]
�F������bNmE��9_P�I��8K��kVO{�'���X5Vyd���THm@pp��k��<H��&ToD=�:j�U��$w���^;�U$��NyX�G��+���uY�*�>��${���X��G&��B5\��-����(%����b�����o�����z�o*��uj���y�N�T��G�~�|��|���y��C��o$�||�O ����w���g���oS6J�\��� u���������fH
��#'��AM��d�'�H����"2d#�
|�s���Q)i������G�X!H��������m� ����f"�|��y=��}�Mxu�A�j��� ��I���:��{$�N��$D�$@y�<��GX�o(o��c�����"��"�@���&0@����Ol���u�0@pH��De�	@�A���}%��+����c�1�Kr� :�����o�"	b�q �X17�|�[��s���5�A��@@MLlDq(�e<#�W#P�`��b����<|0�C���8���DH<��Dm%�[&q�~��O��J���Ii}0G���� 1���$����7���/��� ��}�������b�bz��%5�\�$#���@��	`
��
`#�DC�(���z���@�"%�"������b^w����U7��8��{�~�H��C��d%���q:�>���%��-v~���E0=�H��;��"�1�!��3�=��)�M0DE�fv�
�Kk���4��e��}�B��0=����%��<w��Z��2i]B{�'���*�������wA~���s(�k������*���qF�J���9y�luh��q�4�Io1�����������)"��GD�T7DB�
 ��k��

�B&c`�sy�e	7$�zoU��(E4��Fu�-�����&���d����YP%����������}��[���6��-�!���g4n��V�+��g��1�1Z�[�X%�������/���{��?$UY�r����uV�Xsj�yS����g�����?���A����R��k���bo��ZB�������7y*���%��q�b��p���V
������*OT<�T=�$�i$���j�]��\���m����7�%�yS�����)�\Y��pn�}����}�yVwU�����W�UI�nJ�\��NO�NO��;6]��a�cf��yl�����wF�����t��O���w����]��-�`^�emX5T�{��Z����w���������P��n�_�`bsj��v3�t1������������%�T�������<�<�T��R<��X6���I��$������^���~��w��I}2t8��1���@�{�\:�4�:���+~��?=���������Y-�C�P�U��T<�U���X��|���~���/�X��.��j%6�����q�kC����<�D�����I5[���=�`/*�W��C�mY�I�_^�%�88G���';��]��:�Ah
���V�%��{�@�#�����=����|���[U��w����z�R�U���T7��O�'�������7���8�=A�������o����Z�v�����mE:�1�*��O��V���5RW��^V��I���e/Wx������.j��`��TT�i���2���gjS����������;����-�#�����Z��P<��A�������������LDrB)����D��'�IC�2��?$�`�0G�D�$D2|�w������LH�l�!K1B����\�%��AlA���G;U*�9Sx�GR}16b#����x��H�`��>��1�}�A� �� :��S�fQ�S��P��X ���j/�|�}s6?0q
��vf1����[|��E!$S�)��P\����LT�R#���*�}�Rqj�����Jy>� _�1�|���1�����8��w�� �:LA)��`9rR@RA��h����of)��E ������$C����c��8l��s�O���eD�w��
`
����� -1b �F���}����������DG��RT" u�.��g��������0/;+D|�DkI�����R�E��F�IO'�(AH��N�Sr���L[R(E�B=��@����#y�J���1�JD�V>`H���]�%�L�^HC����0��"rH	H����>�[s���s��Jl�@o����DB�|�i}6���msj �0DS(~���AioL���WV�O��)K��(�n�0n��=�9�u�^Vc�24e�����i���;�v��
�����`v����������I�C<�������m��������F�{BU��S��S����������Z��zL�7G}�&�N����2�/�������+��8H���;Z^�=������V�fb��[���%\i	t�b�����l]�#��U��|����-��F�x���~��}w���y[�z���R[�I��Z��uR_z�-�IyT���.s�����"Z������m�]���8��.*��!���W����,�{�'$����+�T�����>�{���d�E�z�W�f��#O^��������g�������@��y]��5������RjH�r|rUR2I������R>��o+=�g��_?~>�������W]�[a�]N�#x������l�&����oH-8����|ZVOm��Yy�%�T��������O��~��~�G�R�����3I���p(�hg��9���r��Ep������<�P����y�R^���VZ�G��*�������}w���yOk����.��X=������_yY|���LW���u�U"�W��%TjD/*���-���U��T��g����-��+�n���N���������Sh�x�l�V�+�Y�t�v��T3���e�PV�Z��K�����Y��xO���~�������(2� }];���^�iSB0��
����{�~>|��j��C����V{�I��'��e��������~~�����lul��[���]%�������-u���k��Z�������6�J��$���j���<��}�B���<I��O��?>ou�b��GN������������#�o>'�&��\X~��%�P=��'{�I���s���z�x�C���<�T������������2z��y1�#��A�)3��>���5�"�1��F{��'��&g�A)�s����lw1RD$���~���by�X|�|�Fu������`<�y�L`��8�zBz��<���H��`��5��)0C�n�e���~�c����<���D
��8���[}�>���f��3�iDCuD�Gt��`��qS��o��D�|����� ���#�)
����<���6�"d�}%I>����c�v�3�H�"�#P���-�#.�k�?RDw�HD>����K`��T�E>��1��zY���`�T�����{������>�">`��@O$��#�ARG���@��� �R�O0{�b/.{�`��!B'��$A�0GX ��6�K�o�_\��`�`���:��D�S<`���cs}u3�!WY����D�E�u�
m5���V������bB�B3�9�������\����d����#���Dk����H""'� ����L�b#�@C� ����|�)�+��@R ��T|�F�A�X";[��/+����u���,�l�g�r4JN��r��@�S����`U�L��1=�����N����YC%�yZ8��G�Jq�3pD��[^5l��[ccQ����V��kY��
 ���� �zK��I��q��?Y&�^X?S�p��=�z�6'aHwv��N%:�o�
��'n�2T�����]����v���+�������/��.�wm����Z�����YV�$��>��u�<�#{u���s�S��Q�*���yX�T;��/*N��/*�����z1�����>����_"�L�h�jg�����S�ir���yC��
j��T<��O�y�T)$�����nO����e��[�8d[m�^�����#�]��[�CX��]2s���4m��}E9'�F)'�7��Rx����=Ua�j��z������b�i�P���v��V�U�cN�:,A�G�Fv��h�(F����Xy�R{�V���V�Vsj�SM����'����`��
0]n(|���	��5_�]U{��/��$,���^�z��{�������C�������j���=j�:����r�o����?[����i��N���F�n3���9O!lW�j�^�N��~i�>����e�"�������d��C��yW���9����^������[:�"n�5T�������lv�[XC��������-X�VO=�a��gz��Vu���Tj����������	���������7�k,��G����[2�_���|��Ry�V-Y��u�M�>vII6O���j�]6n������G:�a�[���*7�v�>��V��y��&mL�($����O�nJ)��uV{j����U;�'�����	�{��>�W��u�Y�{�OO]W|��� �A�r�h���U�%��S�V�V�V��Oz����������Jk]g���0��� (~b�)�����~C���A~��7��DO�UQO����"=1O$���}pZs���`����}���b��A�JbJK����^cX	x���su� ���IH���������F1�?vb"X#���A�:�����C���1SS��" �0q\u58��� �@5"#� f�D�$������_II�c�d��-����F��0E��ss����@���O&y6�1�A��w��vOQ�����vf ���"���"-�+������M���H��" 8�?1lD����1�Lw�N+�������b|�z��9�1IDG�M�c�S;q�[�x�|�q�*d#@<�D��QnL���r~b��C�����$G";sE�9D��u��.f ��|�GL����@@\��kW���������cR�DK|�iw�[}�z�����)|�D�����`�=Q�OP�&��*�jDv��9��H��z��d���6H�|�+f��V�J�#��� 
T��X��9�X���u�Joj~�)�!�	@)����N�]����9����R�i��-s7�fGv��z6��ln@X*,���zkb��\pS"ot�U��A�|h�W��������wW����^p��r���E:�(Wl������F(h�CC��a��
��,��C��s����=�X�a:�(#��lxM�V��9��<��J�r�5�eB1`;�P������q�X7B'��s[��B����n3����*�&Vd��D����md���yv({�o���Q�%�g<Oj!��>���:~����7��~~���������}�C�j�9������+=�T/5g��xN����x�s�<���A�#i;��&!���P����k�����#�w5`�Vu���5g��^�F���
9>��s[�=����VM������hez���Dv0	}1��������|������+;�T<�Y��z�#i�_"��TrI�)��'~|:�z�������\��u�0��,���o�>����>���wv�rJ�c�������*��U�����T��P��u�E5S�����MT��s�ts��y�iq���WDyE3����;8a��)����>��sT������5^���T9�'=T�~�����_�{���`2���}�
�\D�|e�Z���4�~�"L���y�TnI�-���''�9�g{�B�T��VN��^�I������
��Y��*4"�N�gg�Fo�Ym����LpK<Q��KMu���[V
���V�Y�j�{�{�@3vO�l��{K�X�u���V6���m���Xn�aM�6���Thc����O�|~�~`��Y}���j��T;�V{z�<���j���VO�������o��
���w%�z��k�%goOV�$��w�v��K�g�$��Q�%Sn,�uVK�U%�Yz�O:�������_��w���[s+0t� v=7��+=/��A��[��
�xl���@{�������S���j��O:�2��	���M��z��;��>i� =�����0A�8��-1:�$s�l��#��,�u"�������O2��!��� b@G�#�#�DD���XnB����A�LDu�" �R������$��;��?��`����q�>;1�o$#�F����?pg���}�`�!w!�k��^4s�
D[|�X��`'�pE%>����I.#<����>H&�>O�R#
}_^�w�NI,|����K��w(���������<�m�of�$��fO���;�QhG�A�"1��W�B�>c�}~��-��� ���5����1N}w��%��("1�#�����-�N!�#�=T�DG��p�)0����o�k��byUq�����z�DN]$>��Ilq�B+�D{���T���"����<�LD@��<MW�I���>�����rB>b��95�fw��1M�|�T�����1X�tCF1>uQ`1�zH������~���IV�{�mU��|�K/"5�L���O���#�{y��Z��������������'����}E15�frb!T{0A����#����8*�~��.L.�n��~�;l�'�`P�,�����uc�E�R���"��\b��B�kY>�:�,����D!�Wm��K�&�8��l�I��z]a���y���l<�Q��l�j5M(wx����&���onj����X)�XSn`��w�F�=�t�aW�� �A�g]�����M"���2�F�m��s�0�^�x�L@�0'�<b���WR�w-\�y�1H2�6�}��#Zo0�Z�W�#�TRIFG%V���-�����w�[�T�q���!�'U�ys�-\g+�B�T��i��$���U9=�(�:R���G1OVK�U�j��mP�����V[�$��G�O_,�q(����2�
��d\��9h8�'����*���n���������������*��mY+UR�Rz�P>[���{���|�e�]��y�Dq�H,�.�����-=wF���I�8�����2_UT.I��6���g��g{����{����0�U�W�W��Pt���w[&����b��x>��^ivVkO�	�=�>y�_��UUHy�U���
����=����g������������x=��G�����[h�$3���s�P����Y��Z-ng���}F9'��{�T;��y�wUgz��T��Ob����t|F��_
���!��t���D���>1�����x��3���uY=������`��u� bK�o$�?i)��! �y�J�a������>�������~���~��Xy�T�wU����Y��K��wU��V|����>�~�7�m������+���g�7O���������~���`�Vyz�{�^�T��=�I{���T?����5��y���fb9��t����&�����z�'�L���h�:�����$�G$�Um�������C��A������������!/���>��wv�"fb ��|�^�\q����u��U ���}?T�G�ySZA�/��{�n����Y`#�����%Xfram��������V
b��
@�9����^�P�o�b��R`�>��H����l��~�")���b"5�<���I�9����1�]��������X	��-���2q��T�{��
B"�`l��ZA��aC��[�?Mvb")������B5�<�����>��]������e��b#������<��Tc��@v��F1|��F�H���)�S6��J��b�'nB��8�`��x���]���s��E��D|�`5"�� �@q �AH�r�&���icH���$>���6�9�O�>����H�I��L�!��I |�c?_�����|�@�X���;A31���H�O�bq���${�$�@h�F����DS�Ry7O�RA�D�B:�50E��A[U���}�I��>� >^H���u��8�Qc�!����D����A�%""��`b�����������kF����s�z�}����e~����GU���E���������MA�*!.����r���w/�X��dY�^e��.���TR��5}��G��� L�	�����z��Q2'���-��	6��S�jf��q�,�i���`������
���������_>�U*a�r*R��0���d�����Q�O�R�[>����R�����Yw���9�RV���%�r��q�W#*�s�������ET����@�V�
o�n�B��%$�|��U�R��U'^Tj��P��O>�~����<KB{0�+q�X���d�7��T.��{�NSqH�Y��5��j�{���/u��j�z��U�� E]�=k�&u�\(�8x�Wp���qDy����$z��(����Qs���N<��9�yuI)%}rQ�H�������d�U�^��d�~��eZSI��vw$X�����$�q��^���c�ij�@�Wk���i3���wU/*OZ�9U���<ZH�Q�|v�:��R_9��r�)����,<�����5W��f��YB#�/���:�XuU'��'��P�VK���$�����������xs&y�����P���^�Zm�y\����zu��w��7�����y���w��^T
�P�U�Ry�����uV{��w����������)���i=,�v��n���?f9���*B��c�����c�}�|��}�d{��������U'w$�CN����'�Y���������4nP]��6R����*���s��u?Q���Vf���RO�nO�$�%"%��N�����{��wg���_��o;������&�U�_u���%���^h�\;����{,-y}�P�I_��@���T��Xs���T�mP��g��m��eu��%p%oNZ������u����R���zuBV���w�2����I�����@j�;�R_j���d^e��.<e
bny2A�\��D�����J���A�O�&�'��Z@I6`���2B��|�����y������b�z�!�T����G�'����u��I;3SZA�
LD^LG�'���3�S���O$A�H���QSr��%�j2�f@��S��G���`#�2i	By��O�*���&����Dcz�
0y3��Asy�#�"#>�
��M'_vc�L�vB.��"��"(` P��Q�
b�� �P�b�|��Z��|��w0M=h�9s�"	��DZD���m0����G���6���u �I�\��LLT��"-�y0)c����e+������D�D���tb#P��K�N�>y���3�LA�1��iq�9��s�������>���]B'$�����Dy" {�_P���Zo$F�s�W�0�0\�.�9<���H
`!�:�}-bS~�{q
)O$�����(���"���#���S������9��:��!b ����1�}>� ��-��1���A���P�fb")X#����V���HZ
w�(A���b ���%����{7U�����S�m��[>���>
2���`q��>�JU�-@\�!�'{w�
����AG��d�~��Z�R�'��c������O�s����c��ic5J�N<R
����t/u���/Fn��B�n�c�����zY8bt�G��n*�s��3K��yv}+$�^�b��;��;��e|�������-Ju�|}v^���yu^���a�38c�,s����c`�w�Z����}��1f=#��c���K�{���nJ��������s�a}�e���r�����m6*pLR;��9Z��F�^��{]���O��fbX)GZ�;�F��sV�C�Z��V��'���I$�d�>���]��x��W}��������s'b2�c��v��,i��P��k75���,��RO�.9*�RGZ�UP��N�W�U`5�~�CC����/�m���Z�%.s�j%��@U��8�`t�-f�Q�����'a��'w+�_kT<������%i &S�G;������8:*;�$[EA��j��c������R�t�T��~]���<�P^V[V<�/���z�:�C��a�j��������w����N�z�9n���m�����{n�������qI�9v���rO������yR5�s�gz�z�X{��6w�����vs��5���5�u�Xc��%��n6���3F'��K��@^�>�Y9U^V^���U����s�����T���2�,c|��N�s�WC�E+����c�2[NW���[Vx���j��d��J���Y>)�)�v�s��dAE�O���{*�X���g����CP��Im0;@�@����+�d���yY��I�uXx�R^�I�5}Z�����&/�����FM'�+�],����x$�sM�i��yy���|>o����}!y�
�XZ�
�ao+'��C�yY�Z�z�>y'�w��7��@q�
Bd#�`�H��vA��q��|��R1�I�T�o���|{�_�����C� ��� d����f`
J���;RG������od�DjA��B:�;%1�T}2W&�Ozm�3��`����d�<�"�b �
v��3!IRDGx������}'�����#� �")��$ �D�G��'��n_�T}i?!�$CS� u��[W]��	u�e�fLDU�A��Do%|�,}����L�^I�lD71�PX�<�����z�\}���q�#<�C291�����d�>������q�����@kLD.B�F8����L��
��DZ@D�b ��|���{\�����Dq<�B�d����[��%w��
~JB��X���� ����9+�|�gRg�q���� �H<^��"C/�<'z���]@�5��D_d�)�/�<�
J=?Q�<�kG�!A�f#��EI�;D�y�B#X���A71��Ljc���^s:�,Dg&)���@[k7 {}wO_�C�/�JA`#����H �@F6���;��/z�r�E���51����p���X�x��������ne�������4�:����c\�y��<���o��8���g�'�|1�����J��E���=yG��sM����b�N���<|�P���f�;TEp�����������V�@[���f�H�9��p�
r�+�������w�(W�@vA]����Oru�it��"����zsk���=�n�*N�e_ ������I�e���p���Ke�/](����_v�x�0?_��c��~�}����|�mAj�r�w�P��g^�/+;������^��w�X=ug	%`�����KA#�������;�G�.�
t��5$	�>&I
N0�j��U��T��T���c����;��REm�
;��Z����g{�U���������v��x��mbYyXy�����Z�����<mO�7vO��N�����/^t���ZH��T�[�{(3pXx6����u��h���������e�VKUd��@��~I!RD[�����qC=sqJ�o_�+�E|�+����5�]9��v����w�}�����=���T����{���Y����Ud��9��}}����y��K}Z$���k��[�({��������2&u�������U���[T�U'��I[T��Y%���vg]q�w�:���s���U�+��e��/���_*����������{�*O9��Y}�C�j��TxT�'�&��$�����7!��'$=;]��y��������%[�
����+g/m���t5�_�IM�_��5a��@��V��
����?U_�KO���~����
��A��b��4����h����W����%N��y�C����V���j| �$7d���C���%
{Z��x��g9M<yY
�wY1gu�������:�h������rJ�
9>��>+z�������W���T>��w�2n<���;�=�GF����������i��6� �'��q�e�|
2�p'^��B��;����<�A����
0X�1��g����T����P�
 �"$�r�~����EDaA/9~~�����'y�L�riEE`A������EaV7��+k����#
���fW�������y��aW�tu��~�Pfi��w��z�v�u��H��M7���Ty!oS������\S�/�����k�
f�|$
5��|��>�]�lx�����L���}�������
���������a�ay^��Aaa�W7�rk_��!V�~�6���*��w�g+��J���aDE_���=:#

�*o�]S�j!�QA��q�9�+��
�0���BM��&����Y7G)
�m��=��ymOe�;����B�uxOyx���e]pF�jV�"�aY��h6EgI
M�.��2F���yW��W^���y�^��S�WoLY�^JD�B+����)bB�*���v~p�V���)��oyl"
���/s�������5E�U'�s�\����fB�0�B ��#���q�ADS���3��3���AUD=Kw��~�f�F��$j(��&�BB��o;,O�i��-�����U��cX��if��������~f�h���U����M������c �f���+�	����i�3��=��~��F(��(�B����o�QXaO]W�m��#,){����)y��",*
)�z�vw���(
��?viQbaU�}��sy#�""������6��]:�J������?�l�)��o+��7������}(N��	�@G����oR����&u��n57���z9)`�_�E7�XW�����z*;�r;��2�^�UUW���^����y^A����������DaQaT��fL������<���9�B��(�	����i��aQU�C*~�F�3�/�r-Eaagoq�����TEY�~&�����~���}cMeTB�������S/L�Z������������Fd����:���M�!���>�q������e��������(r����;��Wv�>������*�#���7{�k��������������w��TAXAUoOm��<��0,
+W>���y�~�F0��
�}7w~����0���+�6���1,"*
*���0������E�W�B�����/l�o����
LY��Lg��3h%��`����������0��
`j�|��g�W{�Jiwv+�W(;
�&�e:�W��+�n�OF�=����/zZJ[��xe�xe0��g�#EUW�����TZ
"%�9�}�"��*)vz���
�����r�;]����DE������O�k
�"�/���������|�aETAa��W��r�nTQF&L<QH��)�rn�|��Ngvl�Ya=Jc�\]wi�z/����9�O��j���T��_a�/X���T3C����g{k3_��U�������{|2���������5�X��L���
��
�%O��}�p���l�������E�a%���+�*,"�*0�o��;����4UaPXjO�^�_.$EE��>�w��Fa`.�s����t���,"�w�i]�u�H���rgNe������"����F����;~-Q=����{�N���8��Y�<6��������(���o5C�T�,�����}����<m����V�;%��p�:���Y���h�����"G73�}����V��������<�[H�*����M���UaAPs�3��o+2�bXEE������k�a`U�EAc�M������E��,"��+��u���E�{s4�>"������I}�����d� ����y���J�y_��Y��)Bl�|
x��*�u�������v���������c��KR[��jC��,G��eA���Sv�[��*��.��s�D�#�]7R��0����{�o>��cXEaE]���=�����aF��MV{-aQa|�"��
�*��
_�5��UT}U�I
���������������#,"��|��l����r����O#���������}��dRa��sw�����]v0�4T�+FS�
WYc����f�7[��3�aq�g"�B�w�c���l��_D��0^~a��"���7�u1�Q��w�q�"���"��S{�V��""����FE^Q���9[�paDQV�o�j-QDaT����}��QQ�_��|���DXXEQA+t�N#V�I��q���63�l��S�%k.�����I����X7��U�.���|��'2�m�2�@��<��?p\����Mz�k6��X)o�p�����-��z\�����V=^o�����C
�%]����W��\,
"���o�U]��j�����k��NN�9;%
,(�Op�f��QTa�'�U�>�_/DXDD�3��{+�"*��"�r�W��E�����y7R���;��x���7���f�EY43���J6mH�c\]����Fud��z���v�Qc�]�c~���T�
�st���]"� ����MJ�k��*�,*�?V���x��y: �*��
z+/���QU�U	��\��5QUC�s���UQQD����o]�DA`Q��J�+�;\�j{_[a�uV�9����;��	�'8�: ��0�0��g����\���=��8n���{�@�*,*0�(����n��/J���o�-��RZ*����

"o��z������^������y��3��� �����(�)}{��v9]�/&��c��<�Vs{^����IU��APUq��G��6�m�6��
��\NE�1jB���"
�*z���7��<�gf1�f��s{Ox�����0�#���00�n-��_Y=�zw��9{-k[����`RA�Z��I�����c�[cz���'r4�3W�����Fo��8o|�.�v���f���t�q[1R9o���i�|3��������>0�d�j��TvEq�{qz���qV8�)��FY�v�I�^4),��}�~xp^��h��y�����RQA�U��<��8���
�8�������{�Ta�Uy�{�=>�g�!'�w�=�}���DQ�������,*����[�=�a�D�E�}����,QDa��~����?T�1PAXD`a~u�0_�"��g�������@T>�FXw��'���y�������r��7���W��a1���U�W�-�&�l��zB�����W���� �������((���b��Zk�
���,��H��;��m�"�����0��}yng��d��;5{;�v"EUPFy��p�	�3<U�����wz�[������d��U�"��B�#
�$�r�n��Y�h�d��^��;soy���*���
k������@>�I|1K��9v���u,��"$"�
��s���2���Q�6O��ow�7�3t����U
�*��{����=�V7���!:���S���n�%�Y��{C{���p��'s��/-Ye�?yg���J����'/��N84R��V�
f$�����=���71��EVXES9�a�,(�*
����v{�q�A��;���<��a�DE����P����e|�kY7�� �';�l�����C
�����x�=��B��,B(��x}x��S��_f�d��3
**(�+��}?oo+/l�5�Y�Gs��&�GG�YP�

�"�,/;u��=}��{��v{�K���<�9���R����(�����u�2uq��K6��>��h|>�� �)���+�fG�8��c����n�����/���{�/K�T��DAE���9�>�n�;���'3k}:�f��XDEa�S+��hT�� ����\Q����W���(QDXTT��r�v������t�����n���(�����t��gS2TTe`��m�:����W=���������FXDDV���|�-��'ku�}|0P�|TDTDVEa��y�������y�Md���cl���R�N�=�].H�9b�uc^��W����:��Cr�%����o�]�ue��<�^��>#E��������F���1�;���R}m��`� 1{~�0�F�ol����m���ATM�j�7�����"�������a�M�{��aV�R�����O�����(�L���� �
-�o`l,(���&rzu���q��fc*h`Q�E�F�����s6J��+������v<�_���o=&�*+
�#
��#&�\����=��}�35A�l��U3��������vgx]3�'�{Y�w������_�`c���D��}�b��#��$O]h*�������V������K�O���|��$�j��VAE������O���n{�c������;R��B"���#
^�Wu�����Q�����t���uO&oZ��FE�QaaAZ{S}u�v8�����;�v�Kd���!�v�
�("�"����}��(Q�=�K�R���oOSt(P��UUP�_�!�H��0��
���2���B���l��z��Y��E���\G:��W�����6�`�^&�|3K8������\+�s�z�����di��������-1�H����*�DE����}�{���J(��v�N�mEaEX_�i�'�W�����UVN���;������*"�l��|���$Hsr�wm�EA��c=��������xQ��~�����wq��j�EEFU!b5�}������V>������}����AERrn�oNrf���������e��{���{���UXDQFh�����X���y8������Y��s�;�P�""�����.L�!���)I�J�&U-�����y^�M�"� ��-On�H�r�S�W������;F��*�|aQFPa���������-v9^�+�5�U��w����,(�����YW}�k{"�
�#Z��p�
�������uu~�{���>��W�p�L�;��q�_�}���aE�X��aT.�If��r}��w3u�>}M�/�$�UVE���u��L�
��!%2dJ������r��{GO.�#����olA]-�����}~3oV����������*�������T���/b� ���4�/9������b���aDQUy?ryE��QVa>�����AD���g�p�����"{����4�B
����������L,B��\z����k�/�Y,(��
������<����������O~�y���nkyt(�
�� �������+*9W��>�7��s���N�y��aa��`T`W�u����c�NNp�9W�;5\��ms�)QVEUUa:����w�Vx�����������0EaaaEQTQaa���F�<��9��u�tg}��<�{���x�!PX�y��N����dm�eT���y;�����T�`PX@[&�g
�c��$��o������{��y��g�
������0+{��g�������W}S��O6kyw��������m�n�79������=�}����*�*(�0����r�i�;�����	�sN�����,������t=��)����^�	�^]*z
]��������������"�j�`o`�:�����Z-^,�W��O~�>�����j�H�#"#�����j��FU\��.e������*�f;L��Y�����������;:����)�}����p�TPT~~9��0���������^�EHaEU�Y�;rz�ZFgR�FTF�X�Q���a��Z�}���m�6UXb	/���3�yzU����wU��+*���_�, �"�
"
>�xR���EsRp�d�������%Y�3#���(������r����/���j�}���U�Q`Q�y��Y�w�>�>�S���]����*�"�����e�|z������=��Z�7�������]�0�"�"�
0���f�2��/��|;;����e���A��aPQU�a{��.����U.����F���6 �����*09S��^<�����}e�^��6��3�9�QEXU��DUQ��73�sN3�8_��x���r�i��?Qr��':�YV=����F���R���������YqL��X���sp������
�I�x������ow�N��}�OyEaFE������DQTJ�M���#�*(�+��3aE�J����o�]X��^V�f�r���00(���{��5Q������?g���Q!AXU[�5����;���`XTUEUQQn��fs���w���w�l��w�M;}�����,"��/�P�n
 �HSO���x"`8�9�d����QDDT^���jwfY��^��V�z]^����zn(aHUPDN��������}��gp��9��o�����*���
��	�*J��Flm��d�c���Fj���0*B�(+�X�w&o��{�SV�|�o2k
�TaQ�V��v��Z�x%��2�>�!��33�V��<�N�Q�DUTEE�U��gq�g6���fMs�w�0��"�
���1Gv��5D[$�yc���
��6C�E}UU�
�a�AU7{��i���j��X���1���T��<���q$�g@��N���,]���z��p���o�w����xV
uG����/�[�g�}���z��m��~��q����%��l[�!c�z�$`yXc�v����qq���������.~�'@EDQ!V��ON�n�� ������0����7SG�%������",*�"#��=|�UTQQ�s�O>��'�HX�!EXTA����tTDTUUbEV_}�f{�������aU9;S��Jvf5�$VQ�9�Sy$���y����v'��G�����8���I��-6��3U�vV7{&�n���;OC�#�������4��U��d�k��}��
��c�I�h�sA�����E����*�<%<O;-gl��&V��r`_m%��v����`������AZ���2,�naUl7�W9;j�n�r�;�L� ��q�wn��A1�G���Rp��9[���������9uF�+������b?��S���G���C�pX���A4�Y9f�f� s�fMp@2����Kv�i�O^�R��)����nTy%�6T���{0$�Dh'zI'#�09�d`Vk=����l�#��}��WA�x�v��7A��Ty��L��m�,�+��d�V��j������	�T�w;�':�z����������X�=K4��J��5��aE|aV4�7�*����V�V[��e5�'�2�;��X�?=�-��Ef5���JoK\���*�����i���=�z�n*��sf97��hR�lt1�@���s2��G)��>��/%egGl�z{�!&��{������k|'�G����QAs�(,��2��T6�0q��b��Qd0.��$���
lV�g�P;�Ej�f�/�e����>*������S�����L����1���	�Wkh��;�VK�N����x��Y���J�#T�u�{�b5.������]O][a/r�y��Mz��:��A����:m\e����;hM��7u��]0�JR�e9��0���$�����{e�������=zQ*`���#���m^0B<�)��:�O\�`��jM�a�"_r����M51����7p�Q���v��w����wn0"��#j)}R��L|n��6�|��/�>������9����A*2���e���Yx7�.=Ncs���2u����[�)T��6��-����m
��wo:cW\�]����r�`���i����NV�,
b�F[�P�U2M����dnq�v��"h.�{2�<:x>����8wL:-�	�Z848#���y������j�m��!.�gu��C'�v
������m���9F��	7�ed�5T8��U�B�����<[��5���X<y�&+K]�.�I��������5h�[�}_B7�?���
E��7cp�8��N�
#'S��G������E��I��
X
��X5�W^Z7�k�r���W�2���������B�����\U����(�����W!H����"����l&-� [�Z�cVX3�$-��9Pl|9[.������P�4��P)��h��m�
��@�p��|.������7(*}!<KMS��.�(����^�����cR�Q��_\R���te)���P���N��� O�����V��xov���{]F�S��.��������Th��hn��vhI(9���[�
i�9e�E������a����sa�;���d�/z�C��^4.�p����]�m���F��4����������Z�[F�_(��w�%�����GM��9�3NK����"��[ui`b��)���gZ�S���.�����3-���8nJb�<���aYnB��]+��t4y=���s=��,��-�@FNT����(;��L��8�8��^he�*R��*wn���y@�
�e���(X�P����uq�����>��Sb�B���������`��])c�y�k�$���c9��\`��(;�����7���u�M�vkkl+
-�s���:���
�H��ul��R��ah�Sj���e����a;8�����)���Q��5S���a�
�J�S�zl�E9��*
�����3��a�a5����V���XBi^�r�X�
�:�3��K��y����#Xg7�3"u5��w�uZZ�V�"��K}�Q�\�����A�o�&��u�Y\sj��{�����.����8�
���x
��Vm��0��G��3����n��/�j���VJ�n�p���Y�m �:�h0.vO�i�W��5�����P+��Jj*�s3�j��A���9����857V���j����(���L��>��[OE4�m+��H*�Z +^:[+�}X���O�wT7��T��^mv�]�*!]���IJa�+v���]&�T[���G����rti������A��� 2��jb1lDJ~��#L�q��Z�"$������� � ��
����J�Q]'~� �A,��Gb#�����:�K`��6���L���c[M�D�R!@�����U��b�}ulA`���>����'�|�u�40���E�1,�$(A�|�{&���u�#�m[��
B����������@V4Du">@��W^8�7:���u���w���������Ur��`9��y$�1T��L���c0G�"$V�=��[sd���	I�����@�y�-���S��3��S0 �#^��F$DtB����"��[H@|��\��q�7�oX������:6OY���G����""R#���� 9��s�*{;R@jA,D70�E�@@[��}��c>x��B����"S��H#��mk��������|��+���b )������/S���'�0F�@RD|����]G�=��v`-�����#��}��`P�}{DjH���>�� ��Ds��G���8��6G��s���TF�:*�_+����+�H�hp"],�-���Na����N���oiE�414s(���U�����6�
���G�
��;����!�sd�k&p5&����������h����A������m�~�9���;j�B���gV�e+��	���R�����9���]�ql#�����z���c��-�m�b��>4S�z����k�Y*s��:�]^�a�m������Y7�"X��u.��5��%�^���������.�)>�x��^T*�%�V�C�T��d��%}�U�zi|���G��X+j��>fn��V�����2�q�D(\�
�W��u�U<�T�g�j���c��#���rJ��=�7��~����7
��������c�����K�t��e-�������>7i I!�I%�����������z�
��������L����-^jo=�
���C��w_���Qg!c[��o�N6�+���5;�X/*6�z�+U'�T��IM�Yw ��_�x����>��H�j�����[M
0�J�z0uEf���}������sUj�=�|�V��B����C$�8��S��3�@V��n�/�W1V'�:h��X���[�K�����9.|�U��A)%U<��Vz���VJ�d��?)����Y�|���u�m!�����i���u�'��eu���>C���<mPy�N��
�d��V����I�'1��X.n�X�����z���_KK��y��*!s����*����Z�-U�yU���S��<����rJ��������Oz��~��c�E��B��\�1���=~��4}�������>y_�O:���G�S�U%��'��O}��9U��?�xw�}�ym���~���V.�7lT��x���Dm{�G�&�?���-�V�T+'�*O=j��j���<�T:��P�IA)%}c>yC�}��5�B��n��x�)C���84�xmo'<��V�.�C���-�y|&����%�k*��;�����(;������6ny��d�>�)�(��	��-B����rk4xO>�uJ�s����������T�ZGcq�p���wU���(ec�YSo2�������6������^���j�om�{�{{�;���v*�B���.#w��fD�=�s�#��tv�7�X�h��Y�N{��`���v]jR�1����������}�J:{�^��":euM����������=�������h$qz��9R����d�D�*����X>�l-�3����X����I]_-Y}���o�����!�'�����Y�7������:��f=j�Py�g7�w[���m�iw�T�=+F	�Mn��y���g�I��{u#�/�����������1a*��T�}������$i�Ve�u%v���o��r�9�~i;��Kr��jv�7�;����������|~%�P��z�6�*�/r���UX{�Y�������/���h��f��q����v��<�����}n���Ij������
��>�C���U�<�-�����NSm�[��/<�J��}�J��W��lb)<��T�A+��,)\u��X��>�����dm�{�����T<UaoU�Y���&����7��~����|����>�j,��/U�e���%�#`P�s��S�����H3VHM�=��S��'����U����^V:��{)�����i�1�z�#����V���+�5����e��i��$�QX���@����V���U=��^�H��ItI���}�-�X�v&s<�M�wJ'S5$�Ws�N�m�HdJ�=�ye���_uT�I�I�E''�"rTo+;����}����J������v���lJgZ�v��\��fP����#k�f��m��roj�nO�H��8�=�Y�u^�T:�Rs�e������?Rs=L���_]�&(V��p/7 {U����r��~����MI>�5O}j�-��������u���JI�PE�b��q�����-z��(�=�y������n����1m������}U��^VV��I��a�U+VN�R_�~�����|-������q.��/�M���!�6��3�����Z�����w's����I$��qH�T���}�d��d�j�U�;��Xn���;B}��5�|4���������2�CH;w���4����2OC������B��B��I�-�a��u&�xH�\WLYWS���,G���;$��<\�#�2�/1�`��|:Ingd��	�=�3z�f����.g����gV)��U���N��x H��x#���)9G���/����ui�_��$�:�bP����e9{�S��@�U<�Q�;���=��5)N���5^o��{�G��r�����A��gT�[��cS��<�k����xo�}�[(�mt�?bp�e�����8��t��q����Y����T�Z[��M�V�K��x4Ov�+�*T�������k����e@G�i���x+
�#im���FX;�"'��o����4��=���t��q��?=6��y�]Q���FrN���{%d�uf�U��*�"���9���� �.����+d���E�V��zk]��"�{�v�C��:^�>V��FA�=�70,������_P��<�6�U��j������%2��|��B'�E���zw7:�b��A18�:Cqv)�[������{�~yVw��V�uT�{��V��h�<M�"���
$�rq}[�{����z4���S#)-�2;Z����_O��������~i��g��;��������wZ����;��9�C���~~��M����(v�,si��rt�1W,5���C�Ea��-������J�������O-VV�T���<��<��#��z�c%Q�{�tD��Q�z�V���&��eyP8&Z��,u�qz]��4�}j�����I��F����3v|/3�����n8�,i���J���O/]ik��-���Ct�H�W�t����=Ud����T���@�$�>�9*���yL�9�u��=����aQ�����<��U��u��������/��*��*��e��<UI�Z�����+UI���������\���|�-�9k�\�=���F�:������
��;�M���%���/��U��������Z�yX{�U���j����i�����u�����,���a�����������L�\*�:�X�Z�$�*��O�E�>�>	�*���Ry��=����O�o�������3��ee��w������{�['�����T�M:n��^�R�IUrJ�����yP���d���O_r�xz�j�h�FK��
��t�K���j�tu}1�-TI������PJu+(RC��sF`,�y1S��'n�\�P9���-�D�
E(��(��c��m�U������y�����������������9)�xz�e
��v>���Io��=GC����slQ~���r���t8f�du�z
m����YZ���_v�h^t��������u�<�M����9�{�FDg3Y��y�kYuVl���jb������
XD49�)fR}-��#��e���,]����#�f�b����\w��avw�q�6���X;d���v 2��S]�����te�P�w�%��/��<�^��b��J��s���tyI�|���eM�{/�>~�|z�U���b���0����w��:���Q��S��r��5�w�ZI5���RY��O������y��aM;���{X��Z�SrhfT�d9JX��/#�;�s5�X�"���4��yx�w��	V����>����<�����'����<Z�)'��0^g�v���}
!A��;0��`���[!��6�w0Dj<v�L��=�VO�Oy�"���U�R�RJ�[��$��W��wK��"Kb��o�lk�����*�6���f�tC}���^tW	���rO��J����d���_Z��U�^T;���}7���������Q�|+Vh(�����#�p'V�k���`N;�>fI	RJ�1����J�C�U;�Xw��j��c{�<��B>��.I�_
���G�s���_<DT:��(��f�����=j������Z���Xyj����V$0�$6���+SU���2�Z�������{����=u	 ���������������K�U��O-V6���=�W��/uYmT������������@���<H%����W��m�4������v���8D=�u�jI�q��VymX[U��Ty�;���@�n�vn�b�^�^4��F��U
��8eE���������[G��a�Z���I��Rw���Uj���'��'�K������9<��0@N�*����m���q� Y����9~�����v��a^Tz����������yV[V������~�o������Xy���)��k7Y���g
U��K���9���2�E��/&AggUF���U��<�Vy�P9�a�5A����NO�5���=��`{�OD��&x��gb�[�R��;������nR����|=���p����8��u��e
��Co5�q�*����9\x�gN/Xv����n��&(]\��[c� ��/���E��_M/2K�r�}�e��r��j�]u���iD��WVr�]j��c��;���X�;���+���wM��2�X�����^k�c��~�=7�������Ut�#��]]�U	��c�
(��}Sz�;�T
2zX({�v���2bv!��j��`���M�����bg�m�]y�+���$L������)������Y�3C���m���#�����by�t���x�$������x�j�un
������T�����I.���f�K5�.k�8=b/J��y{�*���1f���m�p�.&�l<|�7)TH�p�h��(�����b�������������`��|N
�bf���UI[�i
�%?xw6��G�����]�nq��:Dem��w�����u�"�`w�X_Z�9�|�Y���;�����U'������~���g�{b%���O)����=O�{3-�8^Xm�g�b�i�����NJ|���Vu�������������fI>�*��6U���{N��.�]�j6{s*�2�v�`�A��Y���;����*��I���_mY�VO}U'�T<��?>_��~������E�p,��P�}��
Jz�����5b�p�o8	�r����rU��}#qz�!�*+Vu�g��Y��~u��:�"�l8�1�Y�897�*��
g0*A+�p��t�,gc�k_����T�U�j�*�/r������vO���<��d��EVhz9�����e��l7��t��'��qK'�:��_��������������*7��������=U�yS�������}���w��c[�u]�O��C@���m�:�2���+O�f'�_R�����'��+<mRyyP����Y=j�[R
p��}����1��S;3��\��>�����O{.���n3{s[�8����/����a��X{mS�j��R=UD�
�'��]����aeN)�v�9������=N�/i*]���)���~w���'��d�T*�w5{�S�I$x��}5d�I>���d��xzu����Q���Wi�;��p.$ ����6�#AwFY��'�)$�����;�����;�Vw��;��=�'��@"4Wd�4������u����J�j�IS��1�1�����*�7�����W����<��y�g��|�v�1s.%S�3w8P�+��p�TNNc�@:Wc�'���av^M�a����d����>��Do�v��~�0�X�Dn.����Ok���x���zt�M��e�|>�'S��:�r�8����"�}�����������2^Es��<<+��oG&b'�1rc����3��c�L�45s^5��������F���B�u�7Q�[���S�h�	(���u��*zl���������34d��5L����[+%��K#���`�E�9����9�#��2����i��t6[t��v%�@�Q��vq���ri'�����l�������[���L�0�y�l{mh��TtW
T:�V����:���s�v�:��������yJ�8�P�:5K�J]3���� 3r�>��Yh:o��6m
��.d;{�o9B�Rq:�����������������W���z�#Z�u���S��'��j��$����ow?6���%�YY��P/�W#�,)}{���W
������k�x�~��U���IZ���T=yX{���@^V������_.������v�[�������*����j���]K9=�jE0��Q�wQ�$<�T�'����A�Xz�`�O�sI�xv:��
S��
B��:�i!�^�J������u(w��������~��'sV���z���j�^j�����T<j����N�|��8=�����	����2���N?bq��5�}��^��K�w ��_mS��{���C����<I��4��$��d��
M�]�����r�)��9��^W����z������o��q���|���Yj���V���z�'z�'V���{�������tO^A#�Z�WkJ��,�e{��9�fV��*-����|��^j�U�������T�r��u_�#�
���I�e�`���j:g���2{=����de�E��z~3��i��/U���<������kT;�����'�hcV��3�z�rwF�N�D;Fw�����|rlX�����~=j��j��j�UX/U��T}�@��O�I>���*�66�E�<L�������|���{�����n�V2������~�������T����V�T<z����{���r����=�����d]�8���v"qEg��=t�lK�~uk����B����$�9���k�:�����_��6��*���6�37���
��������=K/�>�����
<[���0L���C�J�;�\��
��5�:�������t��J�����6�X�����F��A����{�-��,���sg����T:\��@zw�D��B�K{(��=f�hh���7���;�}��A���m�xze�������7��WWB���������{:����C7��-�+�\��0�wB�1uM#Y�r"�LN�^�)@a��T��-�`~�Z�3v "r>��-����]s���VV�J�;�����$�'K����f|10��Fqs��tc��z���H����}x�qW����s&S�����g�K�U�����
�U���E�If��M��*�u����w*�J��G���@[�k
�p;���J��9~�P��;\	�;�GFyOh[�����f����*F#�=�J'jW>��������� �T+������I�r��j�uVsT��~��������f��e���0�cYK)W��x��O�8F�Jz��}��V%��I%���������;����=�g��'�HQ��y{=�;7Z�K}�Q�b�Orq~�>�c���V�+6��Wgw��j��|�������9j�-PY�!����w������-��������^�zxz��]�G�r�z.��r��������~��w*Z�:�=����U�����R{������������t��i9�.���k/b2l�9H���u>��_v�8�8.V%�K�s��z�����+yYUX^���W��%?u2��nu�h�9o�v�.9+'a.��?`��2r����y���Z���P��������`�+-���T;��=��O~u?�	\�'�T+{�e8(a�h]����bY���<��q%yd�*�rJ��*��P�T�-P����Z���u��z'�!���M�U�aT%�3tJ���������YH6��1��n����c�����������=T-U*�
��kV��z����'�T�ow���w=���~������Wh��z�j43h!KCTr(��P�D��|�r}Q:���yyP<yY;��{�T<��|&�'�]r��r��qDe�������Y8Y���=�9��:Af������:=����/Z�yUa�j�{�C��@���[���NO�}s�<x��2��f��m�if�Rl���k���k�`��%		r����C��W6�|����t8�]��e���z�nj�
N^OJ��Y.����f��>^�:��1��Zn��nl���h������Z����qtY��<b	�8'ea�pn��
�5WJ,�~���=�Nc7����t3)�k�|�=�h���(i��(x{�����V]����r�����;��A7|�n�q�V�f��G��>���t�U����2F�uF���S��]&�`�����\Y[�P��W0E�dK8E����|�<n�j�F
����&����Ng�L
�����[w����A������R����z���".�;�0�8&Gk�Fzp����&����M_A�,�0��j �Tzw�p�-��<�F��7��pf��bx�k,*��ne��o+�)�h���G,s[3�K���Rz�f�kq�8��^[y��:"���M���dV��}��UC�+;��{�X��Oz�#�P:�B��.�~c����������������de��{����<�xrN4y����������6���=�Y�T��a�V�X��r�Z�{mX|�:���=~��{��3#�f�����iT���w����M]�@k]��|A�h����yY�uP�UN�T;�T��$z&��!����!V�]��"*A%�8&���������Q�]����~|��������C����S�i$z�� EU��|�(t�i�.����u:�U���9���S����Q0��,��.�-�����U#�TrI�r��U�P��������:�$���5�`����{��� �N:�~sHC�[@�zs�����s�{�}��y�^�'���^Vw���2I�RnIT��������O�"�`q�ch=7	�_�`������B��1%�����~��'�j���-U#z�
j��V=�gw*yY���w����������k�M���f_8R��R��Ni�������b�{���NJ��>�S�����yz���V5VG����/���n�]�m�����5�uu��m����y8Zut3f�����l��;�T��I�uY�����C��������>��d�)�\1]���;���[����y�+*��gv��m��!V��>���m������wUc�T������yj�6�'{�I{�������y����
&�]."7`z�N?n^��F��!��wkv��Uf������*����������[�VcS�*��_q���{H@��@uED��0c���zl6"��<�������h�,z�I�/�J���bj�����b�m�4���[X DU�D�/�����X��aQ�`x(H�$H����kf�z-���d�ax������4o}�<�wI������������C��{���*�K��i5�asY| #~��y�����/Tkn�b
r5������'�p7�,��ks�����>���7^�(��g�w���<��P�G�V����k��Z�N��31P�����Tb���v���S�{L��i�����j��������yW�O]��A"��^����+������0wR�]�����d������b��U���`��E��N�X��x�{z��j��}���/.�������U��P�2i�t1&���s���b]�z��{��5Rl�N��Z1Wt;�;�WQb�1
M���_}U'��d��d�������T6��^f%��
�����}���*Su��
�\U
^�����tV��?F��>�������*�_Z�=�g�V/UT��j�sV���G���^����r�<���]���dG��
�Lz��K����(�ea�=�������*��X>�P<z�=j���O99*��$��Y��6]�<}��:AoM���&��S'�M�����dW�������O|������uT���NUN��^�G�������AUO_������ko��t|���A�@�c��w.��3�:W�\��v�b�Tc�W�7$Z�Z��5a�����'��g��k��*�|��ycuCG4��F������s:�5�e��Q�z�S����[����yY[W���S��c�Y:�S���6�
|�o��������mf�GO�t1��������y��J����z����`��x���;��%��U'���X7���Uz�m'�j���9WhF�+���g�����;���v1������u7!�����w?a�UI���_Ua���r��d��RJ-�>����J~$_%�=z�{.|6�52:AT�C����1Y�F��������<z�|J�U#$�8���������j�+VKz�wU7��z��^����\�>�-QT��A����@3�}aEo�rU�i�`6������sU=�T:����:�d
9>�I>MI*��?6x��%�����k$n���hu���������C�ru}"��|vk~�O�~3c(55!Y';�Gen��N�VLR�����7b[���zH����X�i�����)����Q{8r:��C��\�0�:$J��
=�]h�}>��Q�"���)	��xw�v���&F�w]���]/xxMtm)��D�{�N�\Z�]���k�������o�����=��{�`5�{.B���gl��[�/L�,L ���q-�,��fbv����{K.�W=c{9�8�#t�hl6�-^��b�r�[&��~s"x�; H�s��mv��|l&����B���C ��������$��M�i��Ek�����G�i}Ol}�rR�(��������8�-r���w�\)�Tk^��#�.S@���X�}����t������H�����Q6(����u9��Y���)X��:���x�X��:�f�������L��S>�%�w���^���mc���@�'��Xu��U�{�a���:�����Ct��t���x�������A��$S:�}����*\�n����x�/o���Y�T-�B�V�P=��y�S����g_����K�������������L<��>�C��L�����w���=�r�e�]����������9)����Uz�_mS�j��R�U;���>Cifv�	��s4���C�Ls�x=�|���Qg�����UH��=����VF��6����$�&����3�����f
D5����7�=#�^n�����QU6�
����J�=-1�W�}K@^$�w�R^���j��+����7�~�"�;}0��d�I�.�����P���=����q�6��8c#����k����Xy�T=j�o+j��$��Hs���I5
����1�����m<sC�e�xOmv��IU��6����9��N�Tz�<��kVx�EU'�j�������������������Se�a��S�i���c���F5�/sw����av�|Zr}H�'�"j<�C�����}�B����9��C��b�'tsW���W�s�=�=�OP�����1�l�3������o����|���=��+U�Xw�Y/W'��%&��)$�I��>�mrfn{"��JG�3uw.������p�B�J}�uG2������{\����-Y����U���=���VF�P��|*���VA���77�i��|B���^�a��
*����]T^������i%e�JsT����������>��o��57������DaE��O���6rDU'�s6g�a�������+>������*����������Vc��~5aV!U9�ET�	�AVU���w���v�����0�5s�J��)��b����lH�~ y����(��
5V ��J����]�����S��\�W�#�(�.�uC��>���=�������
h�4<��� P�U�D��}x�K�,0��������{��"�*��O��{�������B������{���UVD�N�{���TQ�A[�:}��� �*�)m}�r}��D]d��/�����$��UQDAa�D{��Lh�}9rG=�=@��c|�pv{J�
/N����{��{���Z�,n��7�M��}����z��MN�Y~;����>�w��R{U����;��mm����s�&UVUFgg�*0�UU�E���d�����a`a������^
,(�(�Ns�z��TaUDJ��U}��&�
��(�$�Nq���9(�-g���5���{=�:�
��0��"�@�r5O�F7��0�++FR���f��P�O��^2����j$"D*OZs�3\��p��-n81�+l�]����C2�!���}d���e�T�0P���<�og����S��+
�k+�v�x� (�����}nP���"#,eW��wKEUTG���9w��Q
��=�
+
��g�"#"��,��;S���eZ�����D�{�z��9�_��������;���"u�����J��`�s�G!~���I�I����f�pN������a��x�n�j��>R`�1'7{?u�$�y��L�+����
�3�,"����\5"*��*����Z��qaEF��QrK����"(��5M�H����5��������� ���T�*���0�Y9��75�,+,))��#��~���*����<d��M�}�"y�.�-��)v�c��yP1��{�H����"<}���QL�xU���0����/y���m�!��m+>�W(��|�l�Z"�o=��g9���aQ������V��8*�
�^��d��������w���Ohv��������743��/k�A�Nv/���OyS�UTAp������$��W��7t��[���p��e�+����m��ue����J���i�J��L?l��JC|���W����07�
�2��
�
���2R�cNCN�De�6�������X���U������EaETA9��&�y=��0�,l����U�DVE��9�O��U��O3>��u�'���QU�U7���MEHF��~�>�a�(

����2���D�M����������xg4�-���'��n�U�I��v�{������[V�S�^|���*������}�E��si�/���f��+bfs�<�<9���*����_7k���QQ���{���}�{�U��0���Oyl������TR ����}���=UD`Q	��V������ADT�[�fz�EDUE��r�{��X�R�JU���*�����e�������s�Z	����"u��
w3��r��h���\4(
#��ZH��	F������T�y�����K�����}*yx_Vl�mwV0���5�����K)\%%��0�,-��g�,�QP�;����J����B��o�_*�E�Q.S��n|�(�#28[����,#
��e^s��99�v�`A��{+~����wG�DN6�I�t,�^�9��[E^}L�[MV�
K�t��9��%���o�tL
r�q\}b�N{	+)�k�PD���!y�fu�<h�S�v��]g���k���\�R����+�����y�_,��.����p����x�Oy���UD�8U�Y~L!A�3������0���������J+�S]*��t��W�V�or8�"0�'h��Q�KS�\M��!��5�9���/<.��f����a��.Om
�{LX�_|��'�%�����5�
8�;y���)v��X�n�Q�mLp���B�g��h���7��^��~EDUQ��rN�9���#�������AVx���3�2!!aE��nr_��p�������q�!���g~��gO(���(�<�����a`hS?�Y�D@�~WmI@]l��Z�5���x����JT<��a��Kc�������E����������VRZ������S62�������0,;����},fb�}�OG�(
):�K��.���QE6��%w������")��\���r�1��`������f{�Q`UUV;������{��7UTUE������D��*����>�����y��)C��x�(�v.�b ������A��V��_�u�|���*��b���0��i^�]��P������A��C}f����]�V��D�46Gi6�;�8�%������w�oMTTA������^en�� �+�:�=����aA�a9�������ADHHc�^]^��E�V^��^�L�����y�'�����u�Aa������=�(+
��7�2��������I�<��s�DE�����+���57b:���(�0���{�0zZ�������fc��vww���
 � ���}T^�t�@z+h]�T��g'wW�Pa��TQDEa�F6L�hif��5�(�KY����D�!�a���sn����el����3����n���,,*"*�B�P:yK�	Bt�s���b=��*
���,"��s�w���������g&Y|w�qg'���U�QaQDUE*������'�v��{=����������������GZ�on�����H+��7��DbE�
��t�rw�R�|y
j�g`�m�vH��UZ����V�@�+sw"�X�7�_ee�w����W����J(Y�W��b��{U�W���y�vzU�����������%O�^���EvM����v}{�,B�zd��~�J�aM�<���UFQa6~��[�:�$VURY{G��LQ��s&`�VQ�q��>m��DQ�UFTUB�B�P�V�1f�Y��l*nD��;�*V�aAXVj_';iZ�6R}KF�X��%����X!Q�^�sb��v�v�=�}�r����W��"(�(,"����uP��
T��
�
��E����#��aUaaT���9��3Gw�M97{��p����s�0����*�;X�c�������h!�c��HEURH����l�������y;w���/��gs'X�XDUaav����������9��U�c}�z�K�{��q�(AERUXQ�aC��ewo��6\�yt��W3'��{���a��aEU@j����4
�i.�0�[A[�;^��r�KT�g�/��!)��>-�)�JS��nx�)�L���O]y�54)�
��N�\���N�D5A��Qx���_?w1��������0�y�I=������"0��o��y��'>�TG���v�O��{�%A�Q��v��ru�,,vx�u��m�`E�a�������^��ET���������`R(���(��=��o��aD`EQ�AXF;��^�����y�%�i	��^"�
���0���{^��������������������O+{�(("� �B��@}�f�/�5����&I�_'�E�SK�y�EQ�Q�EF��/#9�w����jx/<Q
���P�FXD����{�z���X��i��#������q��P� ���������vw�����fw��k��8�]���.��6s HXQP<�[17�4�m�u�:�e��'A���f�%�������(�������O�n�w����/����\��
��( ��e�o�8m�;e�+���U/�7e�
���*���+�"���	[��/�-x���c��O���/�^����o���./L�,
�T�����(}���������3|�nNJfv]��m�T��k�%X��K�51fc���2�E�WI�����o%TEEVS5�K\���EXaET����5g~����� ������w+���� ��0��M��������+/�?r�8�g�	F��zWl�*��0��'>�M��K��nO���VQEXTUU6p������:J;GV_V1wP�>"A��}s�{���%�y=�r����K��y�aVX��Vn��R�e�	�R����LTSZ��8yERPE�{����������3�x�}��d���F*"���
�*�����{��{�����'.���m�U�QADa�EY��|���v���SW��{��aXaEAVov���R]=��<�Q��t�����0�*���,
��C�CY���9�W�o��n�X�RKw�0�"
=����s8UM����ll����<���+��
�W��*�
�\���M�[<������iV�m��[mt��y�9a���bT����f���V'����*�mj�/�e���m�)2��!��l#��K�G�4C�3����k������[����H��0�?T��L�
��������\�r-E�Q������{�"��EUwK�L�s���d����e��_�^�DDTAw�|���W�2���c�����;��QPo�/���y�������""��0�+���YPY���+S9�����!���"*�(�,0s������}3��5�����p��i�E�����("�(�(�*&S��s�������������>�����aTV���Q�Tm>����B�����k.��UDQQQT�m�������"*R�&F[��d��e+���Xb�EQXU=]�3S��<��������{�����"
*0�]�0B����A�G�>�x�a������9|�XTQ�=��wR�)wR��t����+�
�����+�kO]U����s��Jv�l�9��d�*�"�� �$ �z����Dw�!�c����;8)�y�j��D$<��a/:��%�WDr��X�{����������5s���������3����w|�G�����s4�*4AAX�JI���e�VaE��=�w�;
��,(W��{����
������V��8,"���ss��w""������5;�(�"���>�nU�H���0��J�%=����D`E�UEU���~��+�0���~NnsyW^��
��"0�
�Hdxtn,�����(o"��fo.��<00�"������#\�,�����a��-ua�C@*������
)��<��:{����2�Z��������O�uA�!DTXVc{\��v��p���w�tl�k=���s��T"�#��
��6[��\�RK���c��d���}�������"��^����Y�g'+���aQQE��aXAHNs������r��&�{w������^� �(�*���)>�s�O������xt�y6�6���!UUQK����]T����z����"s����
��ua�UhJ<z�G}���u������I��v�!�-������W������
.?ta����|V4w��7��>�=����r�o1��9<��D��l����ws>AXT[�^�u~f�wg���"�
.~�+�8���|]zL�XX�FA�M�r1QA��>������B0*�}��ra��|�bFXU!Nfg��f.�dWc�*�`�P7�u���T(��Cs[���y���+�w|���n���I��{������Xa������YM�kD�x�K�Zs�s��=5�����E�L�!��/�|�s�h�nU�U@�+
"�
���''�{��9���.i�zv�o=��m����*"�#����{�����v�h�����_
TA`Q�b2��J�5�����Y�����7-�fd�M�(������������O}�N�;~����������V�}$���nm���aXAEDD�N\����h�5\�����^z�/{�#���, �
�����{����v���/��&k��k=�w�}�[�#��>!}L�kOF���}t1!k�K|r�}}{m�_9��������t�.Q#~���Z�;�b��p|���\��{>�=�Jc
�7W�|�����5QUAQL���}�'v��T!�Uy��/>�B�XD^��S��4Qa�}Ewc�i
���/��j��TB�����5aaA�<�r��}���b��"*������Z>>���w���{�;������� U�UDP`o�S]��������s<_�x`�F`AaEa����c�E:$p�����[9�\���,*�*0���'W�X��,`l���h�8A�*
a]��#�+
�����������O���Y��*e��0���(R,
 )��vfw�Lq��r����L�79��IUETUFo=w\��(i��7�Y���'i��}_P�*$���~��W�����s�.���N(�����@� �
���v+C.f�!M�%
���^�8"B��
"*��N��{|��Y�'<c����\,d�E�b�����h�������X'�����=|��������Q��wup��Z)��E�u���0&��p��)I�gg������vs���<�*m�T�`DX`ER��_�y_����TU�aUU[���ov�PaEQ�V!�������aVK����F QaRTDE(��{����Q��Dc��W��MUUQE����TWS
!T`a�U�E9��������o����B���)��B:%F��=��Y#����}��<��ig�qe�����Y�-���f!s&����x�LRi�j�u;f���qG�����l�6�eW]�����
���w�!���#7e�����N����'5�,���ZP7������"�(�#x�\�a�b��\@�OV5;S��,�A��0l=�f"�XC6�l=�x2 :{�Z��lLe�����H���TXB�W>�L�/o3�\������]+<��%���<dA��\���n9V��C���+��������f���gbj��t2d�����x����,u�� Zc����-�N���l�N�\��^D��,���t��
�{S9e���������i]�o�E0DXC8������d��.[G�<�C3���M�+sZ��!T���(��UA�TH���r��������V	Q.�O
�-��d�ap����L��i�@{[)�8_o0��U�-nl�z�4��n�J���������}��o���:�K	f�������8wJ�VSW�S��P�r���U��b���*\���l��X
����K=�O)Eb�>g�����ve�\�
$�-}����T������Wv�I%���T�����Q2�a�v����������i}9wtK�5p�������z;x9:���3F����k�C:���l���j����;UUJ�c��NM���'rpa�����<�;�(��i��a�9�{s�`v�Z���;]
Oz\+&-���[�CF��|t3F�������%�N*h���:��Vo����:����xR}����9V�����B��]�����%kt�c�.�y9�Lo���^�v�zWr�8� ��DBe�����6���b����M�ZL��v���N��Ho"�uUne
SD���E���%��]5p��G{�Q�&	����//P��U�G�2�f��iH�-f�����	�u�?�D�V��]�����sOV���OI.-]G}��P�0�W�*��.�c
As�3t[`�!v��8�a
so����/Kui�n�@�B^�v^�4��;���BD�`�8���b�J��d��twyf����V�O+pii�.�Bm��'��r��\9k��M�0�/�P&��<���u"�=X��5h%���E�d�:���QN�t$������n�<�n��Yr�P���5u%�Mk��h�,���b�lq���WhJy�fsZ*���C�r���&�����2S:'	��[���9q��
L����h���V�\�iTY����+�M�s�u�_b�t�jM�m�����c=����r��n��2�@�n�[�W��M5�
�c�a-��p���1p
�t�
ae�-�����+;����E��������Vrk����y+ov�����#�l3�T���mw/t���Vn�L�%��n��n|z����hP��M��u�X����p��<;�I�RQ`��t���
���I#m���+k%� 2�P1d�4�o>�7N�8�Y�NI[B�����B�Er�%�v���������i'Y��X�@�YivCY�b�]�������t�p��@�c^;G1������.����j[�ina�-u�L7 �+n1�
B�	h������Wk%�9Rp�A��,w�����{E����cM�	�m�<������/�kYg�b�N�1;����%bf�p��'(��z�8������������TX���!Wq�ags�����se�G*���]2��;�d��!��R��,��07S%��	����7��^(b��-��1��j�_;��]L��11E�6�B��"�^��P����=
;�i�5���t�
B�"�e��N�y+�vj9��%�|���#1I�!�)-���>hdJP���i��������������k8*}��n�D>
��*��F
fe�,����*�m����Z����������������]y��$mx���e������,�f��OW($��w�����lY�$����UX�������n��
�R����oA�,�l;m�U�^Y����.�r�H}�<��dr�e����	w
��gM(��5��7��5���Oq�\3F6R��Ur=^����~�dF{���N���4��Wn��[������8��.���������<#�j��!C�����wj���a�z'q���f �����.�R�f*�_-�������������r�p����,���{������Wk/
������K�D��^���j{�MGt�Q�0rh��[����;�L���"��&��:�;8�(ve�`{������G���Xv���SOw�=�3�2��������o��J+|[������;k/�(vK�u���VM:�8��O�������[�*���@�{p�U�>�PWn�1�k��;������W��T����.�����Z�����/*r�L��m`���Ag7��|�_�6$���U�s M��M���j�@j	n�wV�V���9����d�}���h���u������l^k�h
{���u��@v+�y[3����\4����r�����_�\f�Ww��k������O�rO�^�*��`�*O*��U�w*Okr��v<�}C��G`��%�73)��2H��.����'����D^T<��%�^�P����'�$�2I>I$y�Z���;y0�,S]��}Y���h6�xR�
y��Y)K�
m�}t&��z}����nJ�$��)5����������j�z�R�|���l��[)�S�)���Wd�����Om����+�]&���kD7��������/��=��5V�T<Z�����j����?}�������}����]�[�X��6�����l!K�Q<��|>{�|o�-U��U������Vj�U���Ud�}�|��}|�����Yw��)�2���V�Z���K�k������D���2���������D�NZ��+U�j���C������zf���z��\0������j�|��l�������,:���|�r�n�T�^T<Z��VN����h����:����\+Z��
�HN�q���`L���*���C�E�����{�'>{�4���d��R^�e�X{��-������`������5�^��S�b�&bu��S]��j���g��������u^o%�����)%Q.H�m���Q������mP�$�Q��b���)�v�u�
� ��:vS��Sk�����n �t��2���Y�*�I�UI�Uj�c��nJn9>����������5����d]*�,yg�B�:)��Q
tn�~�;�b��=�k������������}A��5-D]�����u	�V�3�����<���1J�r�Z�u]��;�mn��up����N(<���8O1�������~8��"�#LW���������{�r�8�u��{[W��"Gm��]�1��{����v��T��=������y�{e:L�#VUd+D�"{q\�^"Dd���y�������b�(��P�Bl��#��
W9�Q�����J�h�&�w(jzOr��aK��<]��������V������E��w���#G�`��h`�ac��oqVYs;R��>����9$:������h�G�rcFv��w����j�m�~y��[�����;��
.bX�[�1g'����`���o:�[o(+�d	a��C��R�0��R�o[�)g2'Cp_U�U�.�W�u�z����mC�s�--hy����|���7���T�`�T:�F\������mI*�`&��w���J������G�O=�����<�������wo_1����-�t�U*�w*���G����/uP�����$���%��M0�w�G�e}�#�	������P�W���Y�;�k�v��j���'������*��[I|	�H}��_��t�d�yuG��0��U�Z�����W?VX�Mg[��y����~���mRw��<�V^������U���<�T>/�|���jc�srP�=����O]C]L��^S�(bH�yZbX�O��@����'�*��mY�Uao+�������8����a��xZ�tj��V��oI:�{�JS�-���a�����f�����j��T<����yRz��L�$D���5D��������;�x����R�c6v��;��i)X���W��vI�r����������\����Xz�X�+�T��mRW�������{&���q�Y����J'�w�U9Y�!��7�������Eo��o>�UP�VU���^�{��6�"V��RI!���I���Q+�.d��h�y'U������N#��X��]^_�~�y��yXw�R/U��V��-j��/uXz�C�������};�5��~Q�R��������D$U����2���	�m�n`����[�>T:����S�T�z�O[Tj@���
��b�R[&��P�a�E�\i�d=�Qw5��{�k4��b[o�m�Q���z���O,c�#+�#��iS�����^8�$�����I�qsD��^F�u�:R�v�a��<��v�M]����]J�"7�.%�/�]TW�`��vh$�
��	&�����r�9<z������<9������������\������������>{.6�x�W��\�A��16��=��3'�8J9)�=���a/e�g���F|>���w�H����j����e�`��+���DZ�=~���vC���a���{M�l7ms>��V�M�3�I��Y5Lk�w)n�)�s�x��tO��Q������]_T�o��z���E
��X��Ck���LV����-�h`����al�l��,�v:�8s�-}�5Sx�]{�����'���C�D3{�E�J:�7,56WryH?^BTQ������7��()(�Yu��^H-��K��%�M0`6�vAiw^�z�GU�l��A���N��j�\n��n�F�n��o�>���O+V
�������Vsj������9�`��]�y|o��;��� Pye��,P�x:��.����������|�s�����*���/���U���^�������������>0�Y������O�%��q��V0$�����'o�\r|��U%��J���<��>��z������A%y���V���q�s12��vr���v�W����E��tY��k��U��e���T����U@��������<�����~/��gje�V������L������_,@��7�������qw��?}���O[T��P������^�
��mT��I����\|]:�-�t_��z���^I
~e��jE �f�����3����9YU��{����Xy�T������|�H��I�Z�3*����U���lkl�������������R���������~���TkVx�Rs��������VOkT�+�����������y���i>r�G��Z7����wF�M?�P�����4��rI�ImT�*�[T���Y9T���]9�����V��Q���=9������-;C)�����y��7'�j��Z��U���yyRV�'�d�ud��d�&2s��`��LmY�q@���������f��I,���Y$ig��7��~KmX_yRZ���a���yY�r���=���_��o����~b�JrWh����X��&�s���U�0�F�!�y�\!�{"� F������Y��	��z������B����q��T�4-s�Q��(0�����sS��l
�Ly�Q�&!�3wD���q��*�
����{��������n7�>�������u]��/`�����y4
�����1��
t���>���u�����sg(?x\�j;���f�0����uf�)���c��^������-]���6K���uJ�������N��3��c�%����M��SKqf*�0x:�����<�/*�f	��T���uWt���^+m��b���������[��g�gT���Ux����7����L�:���u!�D�n���8Ab���(e\��
�V����!��n�=�rD#��vM�}��1�W4������PweM&��L�(���5)s��
�N�3r��np�������w�:�tVZ�(�M�0�D��Y���*w����P�U'��KmXZ�G�U�@4I Nm�j �z!��u�#�7o�{c�
y�^X��~�^2�ws���������*G����T���^���Q�����{�R_�o��?}<�t���M�\��6�94�B
��j�j�&];�J�h�q��<��NF���`��<��}��r����$����X�@h����A�8J���������S��+e3�Swd�h .���$�VZ�����C�j��jO��$���#�4I��n\��c�<=��\G79J�����`�5�6+�W����F�bS�}�.I��'%Si��VyUa��F�a�j�z�Ig�`]9���i>����a�RLOc�b����v-h���[���IElg|B�U��VF�a��;��r��f$>�%��������~���O�z/ �s/�<�\���S����Q:0jp�7�}_��T��UD���j���KUaz�kT=�P�t�c��|��M�������Et+�H8��]��M�Z����w��O�-8�Z���U��X�T�{U������_n� ; O�X�-��>����gY������	��Y|��P�6�'�<���>��z~���UY��Y�5N����T/r�<�V����������/���9K@h��0��E��%�:��F��"�o����Sa�`�+����m�'�����j�=�P/�X*����=���~�����z�}�y��~�c����9�G������V9,yi�>o�S��%N����'0e��of��t$:��	V���12����y��.���^�vt(z��������=��Otc�C���[���H[+�N�z�O"�������k�.xF�e�=�����
�=���f+n�a����U����<��^{sF�������M�<=�������Cz�*������h�TZ���_��1R��,x��/����L��[���o�tv��^�.��g6���dL/�c���[����;�2��g���k����.��+;��$���o��\���c��K!iV\�xr����vWc��&�O�nj�G�����S����'�u��e�6����������vW�d��`4�`�������n>K5�����B��b�L�K�kN��y:�Q�5����U�������*��S�����C�E��j�	�)����/���W���������w�dO���nC�VN�T�T��a�5B�V_Z�+I|=�&���f�V��ra��Q�����4�
��<*vX��]H��~~������N�Vu���=z�/�V*�_UC�j�����?>������<�,�0VpA=�&������|�2����b����t��}��I����{j���'��y�VZ�������1>���U-��
�r�����l4���
^�*���l��������O�nO����Y�Ug^�<��KT��/Pp
�����"r��&+�Ng�m��������7f��a
]�>�Yz�mQ��Nz�w*��U�E'%8��Vgi^������/V
Y
��]��;���yZ��c��i�����g��UP�yP��@��I��F�Y-U<���s���>��xo�����O���P��i>
��z�e��&��K�������o�y�6�G�g�+�g����z��U��Y����W����N�(�i��:�T�8�mpKx��-�M�t�Z�*�f�	�7/7��E$��u����=�T=�V�g������"�+�JwM��]��P�t@����o�����b�Uyh`����q�������s���OyV��^���T�Z�//�^����������������U�3����)�f�P����a)��2�q"<���@d���O�NJ*����X7����XuU�$�^K�I��!��|��s|r-�e���}���>�����������Q�����v�T�^.��h�&vq����c�[�3Z�U��T�t�	F��be���A��O��#i��C.%0�����1KbsR����u9Q���������G�Ars%�I��i�v"g�J"�"�����@xaj���h�������ut75��p�b����fr�@�N�O{���W=k������{��"�d�cZ��w_�}_d��s�+�#1���xU�;n%T=rcvcN�U�<��|q��y�J�J���y0��;%[���[G ������,��m��9��1=Q�H����c�.��P��s���f������Jh�D���5�]�����Uh��.i\Ss��r��X��J��=7O/5��������H3�1�t!E(�I�*g��d�KF�����E��w3�.H���V���QK���R�e5Z6��4vK\�(##��U�4/
�5b�l������!�0g��>���|�Z�;�R[U}���X-Y#�TIj�>f��j�I����[�CvnA��7K���������mc����������a�j���yY^_|��=mQ��m�'��I�������������c���s�q�\"|/���EzR�3/��7$��NJ�#%��>��VKU`wuP��_�v?{q�S���(�]��5:X��8�+u�^Vb��������0�`�Za���������;�#�*y�T;����g��UC�����}�S����h5�
t����(�\4�U��a�:��C
a�dgd���%>RHi$V�=�Rr�;�T:�e����Y�X�A��S-���Qr���Y�V>	lVw�$�b�)j�,�3pL��%��O9VN��'sT-j��ei��"��P*I_=�W��ZY��5�{KZj(f�����
�&VD=x��y>S��������V�6�{U���d�V=U;������{~_���<8�����m�v"]5����v�������k�8�#S-��eW��[�I��W�������}Ud��Y�U����VT���
�����7zIqO��ov:���gm@>1����rp�����I'���-PoU
�����mP�� *�%��c��XT��w54T�����|2�f1~���{���p�c�72�������u���VV����Y'�I$\�$	�$�ks��O+�Q�Z�����)������M��l��U�~5z�����^�P���*�K"�m1�T�`�^�56��.��� ;���	7��S��=�
b�=xV�Y<��_]<�U��-�����Y�m�-2��titVU��d�UOE�s����y�K�P���3��t�Z�$UZc�����5�~�JN�nC��fo��Z��r��h��JN���P�&��<�����ET����=��:kbo���}�U�w(�id���7����5��
��Z
��z]��P#��3�M��
�.b�������"�o�
�T1������S�)���&����N*������jM����S�X;�V���`>gmA���Z�q)��j�u��t3]��w[�-�=�_uv����w��vL��X�x���
*�L�2���{N4�����_+�x�q4��a�WeN�o����C�����ajlE�9Y�'+e��AL��Z��v����P��w�>�
�����t���=�h7�_��|����9�<���{���|"j�D�'���!JMp�/_p�������c��n�vO�d������<_s+.k��B�Tv�����NJ	��^�-�I��P��A��*�
G�2Us���x�g���'{Evu_b~���������B�V����sT��U�Ub����d�T�VUX=U��~|��^}�����+<�_f,�����%��R;��$T���<������%SM�UI�'��I��R������T-���>|���m�N��4���	4�F0s*�V��I*����NRR�����%Z���Z����5Xy�T=N��$��������^.h��-[v���� ��o������^K0$�!��w��o�~������W��mXy�Vyj�>�������O;��z�<��|����d��1v=�c���v���m���\�=�=�6]/�j7�q��^���s��������-�'��=�����}��z��#�4I��R
Py4�K��� q������M��ny.�\�a��B��n�������V��'����X_U`s�C����VV���>����������F�n�s�|�(COh�B+C��4W�G��t������}�'���}����R�U�-Y��=U�eRQ��M���2�>�qLk3V����V���o�:��l)��������VO�:�B���P*X�+I�S�t��[�k2L� )�����)�#��c��w}Z����jJ�O�y���Yu�0�UW�����$�>��]������uiz��L����=�h'Z��[|��2wU�K�K�`PM����l5��q/��%�
YV��,����>��Mv�H���&��d�]����n�C(U��m�{
�~��-��>���y)�~���nD�Y|�V =���t�s�.�rc�zy����2"a�<=���bk�!��;��~y.�D7]�{�VD��#f�_C����;�`�����!Z��F���*+
��G/��|��ax3K��i���Oeex��%��� �^f8!�������w��NE��W�>b5I�>~��o�������r�-P71����{�!	�fS}��������Pv��c������'@B��������<���~M�<�^�N�S�u����0*�e
���<���������nks>����>�3]��|����eE�+~�oR��n��6�w!�������P�������V�U��nH�������g����-�*L(�3�E)��f�goQg8;�c��}�wy�~>����U}U��PmVu�c��Kz�6�=��}��������M��}�r]n\�L����V�{6M*�.���,����������>�*5N����X7��{�g��� �?V�j���(�H�m��U��8{k��\TO�}vy�Gn!���������~�A����<���Z�/�VmX�T���U#�����|�����~��Rs��U3�'7�5v���[����/Kr�����rV%����$9��{Z������N��Z�����3��8�e�)2�����9�v&���^�������5"�����E��z���������R[U�yT�� �'�x�|���8svs7�z�p[z���k�{W#+�v8d���^3�}�v�/{����mY��H��a}�H{�U����C������'��H���K����B"��om7��;����Zn���2�������T;�T���^�Kj��Vw2J�9$�������x8�++�xG}�zl9H�����z��xUK�����/��9�����vI�+��Xyz�>���C��������'��|�����b7��o��85KL��P�*�����K�Ff�zw�k�,8���q�TLk<�T��I��C��J��yZ��I?V���k���ug�O�s�� �V��I�.���^�@����
��+������;����e���:����N�}�7q���#�n�:��(����/��*�e��P8|���{G�\uv�zW��f>;2���P���#���x�������������"�!Q+(��\S�����`���6)���{�;c�P��l�B����J�^���D����M����L��I��k���T��q�[�4�<<0�����K8��zE)~���"r�+[��|����!EV�9�.BU9�3)a�T������@������gG�:#c<�&�X���*w6���+���[)�cw@�Z��
,D��%�vu�
RP��4[��vr��}-���S%S���l��q�*�`�2�����#��"���l,��r����<�r��"�
SX�h����0{o5���|������hd�tL�.��c��,���Ic��e����-��$�y��O-��+�fF�J�\�8b��A�W=�_*��vA�-��\)$���-Vy��mV-XV��U�U`��'�wd�j�"N���j�6�VBo-�u ��6��2�8=2��m�y�}g{U;��U<��=�T���i6��eIrJw7'��������Y�]��^=���5���r:�������7o���t�Uz�mRy�U��R_z�7�����g�{����w�}���K�����v�67)`�Y��R�Ho��j�^XPs����6P.I��7$-j��Vyz��guT�r�u�MQ��\;��c���@�|,�vgD��Az-���B��b�i�5h�\�.�������^�'5Y��Ru�A����nJ�&�E����<�[�������*�3����^�vBW�%���h4k�#X;.o��]U�%���>�g����*�Xuj��������=���>w��K�Me����x������,vn7L�D�X�*mP�(_UO=������x�AmXr�A�����UK&�lT]iT�b^���c���5��z�r���w�n6-�M�F���*�����J�����z�{�C����Vx�Oz�rW�:\�WN+ES�G]�����f������T��1��qh�������|�{U*���U[�V���d�A�'�A�'�����Ib=l�`6H��Ru��$�����h�@V����v����}����B����y�U�oU�j��j�������3��%s���wZVr)�����P�;V����2'�y^��[����D��6�s}�B(@�Mcq�zU�9S=Vn�z'X�9�`U���i�)���S�y�2Y���f`wcVS��^&�R����(�-(���q:r5m?=�H�\�	�{wa{�nOju��UUw��c���l��a���Q"�k���y5��3�qtc�<=������nv��E^�bh0=��k��7uS."(����ar����9�T�x{���U��������}J��Vi-���,������^�<.�n�sa��7�n�)m�so%#�3y���]��������u���N:B���9r�]������lP�T8���T�
��]Z�����rKT����j3/�%\F�"�y�����)m����GN�i�%H�y�(��c��=�5�Y��n��j�������-���u�U���$�iD,���vs�tnaZ�����.�k���\nX�f*�R0�
�I�d�=��x����a�Ue��Z������+V����m����A�'����1��}����y)���N��DN����2wuR=��{�;��/��[�|"rU"��H��U/^n�WX���+=J���y�X���9�/ko��ZAx
����Vbv����&���d��UK�V�H������yP���:�Y�S��%^�����6;�C{A�k��EZsk�o�$����gUX7����@���������{���Y+j���|�����|�{��>��ff��N��ir�x
c�ue:�����e�llsU�]��7$��$���e�T��g�U�T�Z��j�����*]�c����R�=�M!�)b�%{����D�j���d���GXO�Vwr�y��A��yVN�������%�l7��u1Qy��*��w;l��#����)���<��v�J��w����T�}�o���Y^�{mg^�Oz��������Z������>����So<+���/�@3$��u��m��T��\���R����P���8I�wI?{Ua}j����5�
���W4����E�(�J��c��\��9O�cg}X�6}��Nl8s�6��\;B�j��T������[V��UYT�rW�G%|��W��{i,9}��A��X���y�����h7�/g������=��=���<n~�wUg��B�T��X^������mX�T<z�?I��{|m����\Mj�I�p>��')��5��2�1���K��W��}��7��^h�[��V+�����~�f@�1x[�z8_����<w�������*+9\�{�W�(�*"Fq�w�����]��""�>������"����3p�4�EUy���qy����,1_Wy�q�^V0��,m2�~�TVFKI6b{i~�H&�~�������n�W�u�L������8�nk����r��}WY��H���1z��F�x��-����V!|��&U��gg^��d�7wC��"z�������l�I���O�UU�Q�����y<#UF����EQ`D\���t
�,'�W����UE����]��FUT���0>�yk1�0+�	������vjv������;��=*�7���:!��%��>��|�d�/!���J!��x�B2�w��,�2t�K�HME�<7����xu<��(���]�KC�@��z*7GGl+$]�s�}���^�(��������S�H��2QQTV//��������[|��r(�
 �0�'~���v�~jw��
�-�d��[}��M����
s)*D��h>#-xX���WwJ�h(�w����Er�1��O
���pz��Q�a�w]8G�in�����)�Kn���
���;q[��t�z�"+�r��������[_p�����0���7=��;���P��(�*_������xJ,"����Yd�'��
"�"��<���5�*}�F&o}Y�*
TDUE����������^��~��}��(�WY�+_6xgDr������:�VkA����.����0�G���OE���w��vhr}^����6d^��C���2T���c����y;���������o���&i���/������������EDD`Tv��o>��FU�R�x�
��*"�s�w7�^h00�0�*�5_IW�D��
��l�:{�'��QE��3\rk7����������N��j*
,�/2��������\)���^>y�������4�^7�^�S�HT�VoVD������p�fj:t�ke�M$�fL�r�`�*2og�66��sm��9�������������d���2!VQE�;��r���"��{3��mv��������II��EVQATT�����&��aaXA�������|�wgj���"�(%���Ug��XER}�Q��q����3���Z����������u�;=������������j���;,��<)�wRq����K�5P�:L�E�'�z�?
�=�j����4s��uk���^�>��b�#��<I��=��XUE'��UGf�W}���d���u�TQUa��K>���
�XDTDA:��3�g�iE�7�_��=W�v�(�����y9����QQa����{�t���F;�y��mgB+u�$Y�mloc�k�C��0��o�@03=����U��������Z��=���e�����c<�V���<�o��3/�4O�]E}y��w9_`xaUb��o�~���"����mm�Gy��"
�'�zW�}���*}�����*"0������������aC�~���eo��QQ�Y����3�|�'�u�l��������;�'�v��x�Ne]��Dzx�}��=~�{[oT���
���������i,�����co+4�U��Y���a(����������W;������e`��$�����>yU�^��qU����~�����("���^��*�Xa`UY�k���o�i,�y����U�X�EZ���J��}'DEEK�+&��8y}��������� ��"��0���v��Ag�k��c��Yv�>k�R�����N���_r���+�_
�re����ZU�e��#�`u���u���P��!@T�.�K�#���3[���ie��f�3���-��k��|����AaEDu����B��0�l�z�ETTc>����,0���}��#�����8��+
<��X�K�,
��>��x�P(���'g��������Q@�U���Pd�^j���r34��-�m��6E�^��[����Z������wR���6g�4��N;,o���yY���i��y�n�������#�:���T�na���������`-��*���d�=�\��y���aaQa�Q�L��s����APPP���g�;�� ���u�����^��UQTF�7K�������
'�gc��C���" ������|(�(/����zQ��=
�!����q}���� ��k��+��������5omnj�Y����I�!-�j\��S)���"�N�R�{��hY��I��T�%0�d"Hgb��9L�N}�1Oj<�����*�����oo+��(���5/��pEPV���$�baaEaQk��y�����iUE�����|��DTF����T�E�(�*��/����(��*�QC��=����	��p�z0���,�V="���\���cFDY9]�a���a.�a�y�Sj�F\�:c���Y�IS��b�����^���
]
�@P�0x�r�f{����B���^�rs��
(��~��[������,�c���e��"�
��3�����Pjq����+I�QAQ{�������*�*�]������/r���l��������i�D�B�.(`QUaXAAXE��2���LbN`Q�s��3z��{�*0*��0��������i7>�p���U�|sDD��J�����t�M�2js��������
������f>q@u�&$�5�����������Q�`Q��=�������1�^���%�����Q0*,(#��U���:`�W��2j�,:��A��DVUas���x����S�O]�\�������)%��U�^K���e��s.���,s1P"�("�+[����������'gn�,[�5�MD��t}���P�����rY�s���nrx!�n���Cw�^����
�.�n��6�X�pS����G�_xwgL�!�<P�U@UA�����X�"���6u�N�K���"���4�����������/��1b�"�������QXDT��>����(����H�#�,",)S���Q���k��E@EEaAE�\�"�x��30�pux{M�;5�J"$d0�*�������,��k������+��|K^�U\T+� , �k�w�����m9N��������e��s+3�,B0����-���z��_QG�E��c�=�@C�QaAsn���^������+'+sY�o<9[\����0����D�w�h�t��Ja��NW��� �TAA`W}��_5s�M��wk���Y2�}]zU��"�0��(>(�VQ��v_k[Y���M�J�(��0*"����g[��<^�_z���+���B0�![G��cLO&�����`���L�0!Y�Y"��Ik���T��;�����9_\/��u�q>��Zcww�<��������vk�U���3�p�������JB��
��:l�g�B�9����}�}��XQ�Q�E~�_����9�D`UQP^NnT��m���**B�������o=���DaV=���r�*(�+�O&�������]IQ�K^h�f
!Ta�V"�|�q�bo~�����9G���������k�yU!!DA��a}� �ti�,wV�J�*�o����P�|�(�0��U�e����������_|�����QPPa���dxb�L��f�t��2j����0(��
���"��yw��l�R��+ZC%�����?�t����*�0�� �S5[3���F�y�������W����`QAaa`U����������NN�����a��(��**�""��'�>��7�v�=u�WY;�}w}�����EaDTP�!��\�����NY��!H�x�HpQUQU����&��&|��{8��F���d`�QV�T�m[����A�t��O��z�[��|��\�{�^�dT�������v6���2b>�f�����~�7s���m]��(����}����������(���������"����&�}W�����a�*m�|����bQQQ�F�����������g9a�EG{:o�����$
�]o���"m�=��	H����+���������=����U�3�n�}��z���X�z�*("��"�>C�85��=�Q���\c���i�a�EaaF����_y��{^���0� �9��]����

TD`VI=s~����Y��3o�����oLs��TU�XU�P���5:�`0l�TXVE�7�f�On�,aDA�����g3v�����y0q������s��B��+1
E�}�t7|�ns����wi��k���*0,")
���+�
�����l���NY5��"iP���AHE!UU��s�L�;O�{�n{����m�*^+=�*�0�(����"��������������8p�6����=����������7�zYqy����s�$��\�������/=d3S��~vyy����G[Kg3����9
:�d�|���Y�7���e��]B��������-�����B��5�r}��=�8(���������AFS>O����viTXQgG������4�EEa��������*(��]d�(�"�"���h�������XE�A��`a�=�O~k6�3j��}��e�v�����^�"���*���
��Ma�&��v�D�bN�m��\��|�*
�,"���F�h�&�r���mVt� 
���=fB����
��*��oqGA�**[q���ie�e�/�7�������
aVAFU/������N��.h�x�A���
��>&Y�h
���+������8=�M��{��n�e)0"���w����l�A+����N�,;�u�;��, �����������y��3���k��s=�80�,"�"���
*p���k�s��[�3�i-�f���2�Z+�TDPQQ`Q�E��3����\�7�dN���$��++!!)�����C���m�]�$���7|*utbvG[B�_��r��}�S���.���8sj���B�4\V�%�h�g����B���d�����u�_m}�n�7)�����NO�Y*���*�����y����� �����`E_���V�-�Lo���FK���0�
m��$vI�!EQAAQ�uU_Fq���<T[7����s������l�
 ����(�#��rg�.�'|mm��W3�����{�s��"
0� �
}/���Yy�n]���U��8�;��bDaPAQ��oN�]�a��^�}��2l���W5��`aUAQU�C�S^�y�o���|�M>�l���g7�{��Q�XF|(1�pQT��������������k(�������VFN����m����_�����F�����(D�DbF�"�^n��^�9u*M�v��KZF7Q t��
�F�f���W5-���9T�s����n����I�TTTFJ(
��n+��l���)f�WPgi�Ez]q��7���=wr�A2n;�g]Tv����Mc�P5�`�:^�}��8b��t��T������}tE���W�[����������O������ ���"�1��O��T�Dk���*����PD�����*"��*�#2�}U����a����u>�v�E�EQ�aD�}���v;
"�����;;K��q��XUTL��<y�7�h�����J� 
�R
���������{�+�r_h�s����<�Mw�����XaTTaA��������^Y\o�_#�Y�����PE�Ea��g���]|�=�����'�9f�8N�rr��TUD�����5�37���>�e�oE�aHE�s&{�����Vm��'vh����vADDaES���g�v��nXtj�&����H�=\�{�����w}������fr�.���0�#
�����VHE�9.l���P�'���	mJ�("�
0�,0�}}>��7�;V�j�Y ���F�>{��1U�[9�hg�4[��������=��j��~d�y��*o%e�+<�p�;�r��w�q�+�,,#�s�T�_���z<�-v����5QEEEbo��g�#���'�+��p�����EDXQ�w�����aQD����|Y�XDF���9����Q�aQ�Is9���1*��Nv����d���(���*������=��3�=';}����u��5���K��v5FXTUE�Xc��{|�Rm�W7;������9�UDaUHDTaV�����}��'��*����s�"����
 UB�C�����p�ov������������*�����*�*�����vaO����y������aaQAaDQ;����kg����)�v��p���[���E�UQF�9Q7��[�O��s���7��d�l��^���	�Daa����*k��KNj'2�h��2�v������*�C���^s���<������R�
"�j�!�D��HE!{����k�_�v��^�,L�]Y4�+9�x�[SyN3�U�K� 14,E�r�v�����&z�#g���K���������bu�(.���,l����T���J��.�>���dX�����*(�0�����Vz����*"��(�"�����S�6aF!PDQa��}s��$"����345��.�`TTDaDPFi���������*"
�(���������>��XDTaAa�QQw~�|��{^���URQ;��������ER��{���>�+��y[�����d���^b�Z����S�!��>XiS���TX�2��$�9��f��r$v'S����	gw�������]�Y�6�����Sr�.�����6:(�*���Ty]�������k7G���5����:�����6�f�;�*�9xcn�5�qq$BM�(������4�J|]��7��u Jge�R���� ��h��������d����#�U�m=rbAK����>���Z�k��wGC��j|w;n<�)��{1t��eY������:%KO��o5�l8��:V�����������i�]���T^<�KlM��2\O���B�XX&�e���o7�r�����T�����5������F#G!WP��50���%�f����uq1����T�����oD�e����$����R&�>T���6��h�x���J�7���tl���j����\�����������j�bB��x�TF���x�� �P�YD��[����(nM�h!�E�}t�>y�����,��y��+����|���w-�*�&p4�I�� �	�$����1n%��j�!bU8�Ed��6�����j��������Vu����P�2�0Vgn\	oD4&Ul��d��V�omIX,!���s6������]�)���ni�a����4"Q�����p��8-�������6�������������}�a���z6��	e�{:�S6�sy�x�2J�Z��k�dC>�)�s�]�l�-���1c\���
�rm����������h=o���p)��F]JQ6\X8����	��=�,Zy���gf��^�V�uG�np#S�Z+q��W9�L��u�`t0�����6�-����������oV\�qq���B��n���@�������ll)�m�Q�m�L�hN��q��������P���������\#2�@�[5�]K�/3'u�u1W5��(���������%���!
+���S��m��,ZL'x�f�t�b�����ho^��Q���]'(��b�+���u�*��V���4������)�|�������*�N�!��f ��fv������E���-Z�s!��=�>�z���gh�C�8�!��*a����sq�.z��Gv]��V�B���pWHJ����t�r��HR0�S3$b��i��q�x5�AE����w���hwo�&;��,�hb[�0)N�+d���t��9fUm�(v*�;�V��#	����*=����`�������kPni�Sye�v����%���t�o��r<����}�Vu��r��������\_%p
�M���fs���`4�K��YK�,8���V����M�]����0�`����^JUb�2�.7��9jG��T��F\�%K�&�DI��u��]�3����vU��K������U���,b������j�
�<����sR��3Y�e�������������;!��LJTjU�����G8����l Q�2��jeb��r�[r�JB�9`���A4oV�YLXy�#�XV�\�v��I�i�����0^fZ�v��ZL5�6`� ��n��I	R��g �������vy�'���c8��VF0���Um���6��Y�L��=��EK��+�9K`�6��-�H�u�+[����\f�n'��S=����*�6��V���y������:����ta��-v�bp&sf^��4>f�#������*9l��jQ�_F��C��W!m�\' �������3]fmK���
�s%�s���9�;M4���o������W���!a"p�s��,MF�4<�V��`�,w.Q!:g0�jv�<s6�l]Y��\��������R�gv	�l�tQhTY�8�����8n�6���X��M�8����Y1����z�������y��;��na�!���wv��b�|N2��4Js�9�F���1d��������3y�s7���q�V�o+;xkP��1pNW��pT����mp��*�wr�6v��,5������%�t��dV���,�
w�E_
�j�az�������{4���j����[��.R��6���DrF��6��{2���.������:�T�C�m�m��9��*���v����V�~��6L�o�/zX��*�$�&�u���}R��F�HQ7������|�O���Q�Z��=��bEts�Ub}F9���*��^��c|�yBq������Q�r���eZ�����E��OK�����s��J�{pC�;1�R���k��TU+���Vk^.���aW�~��.�ib��;��&0���k����.���[������]�<<��k�y�va��ss:xm��:/7ey�}7DBa��b��Ll��9��4����9[��E}�pj��U�YAr�!Xa��@Wi~�e�z�U���7Tk���HU����������.M���G"J-��i���c{�2s��/D=�x�5��
|���]������P�7�����1\B�os.���<m�j9�V
��NVU��{iS�h���nR}����/�1�\�"�
u�@^�;�9�����^�$Z�]u�;���?~���[U��T<�������j��Ud}U������7�����7���yv�������5Xxd2�� ���A�5����qh�Ef�7.K|f�O1+:I��=V5�N�V{Z�{��������+������\��tW~��u,����[.�I�[�+zQ\g|����>���N�X{�*O^�*�}U����U���:��v����o��?��~_|�	<���=���z�jg2�������~]�����9�9j�}�dUY�j��*�+m��������}�����J-��=���<�k50{���8���Sr��*��U"��J)'�W�����V�[U�US��oj�S<�K�3�-��j��k�.i4V�C��w��n-�5��d�BRuW�O����U�X��-Yo+�V��$����{������*T~�t��;���u��^���dUI���s������~������X=U��T����a�r���T=�a^��>��������!��]C��c{H��������������&��_vn�PJI���%O{������T����W�u@�j_b�|�	������j���j��'eR��v��5)���h���;�*����yR_j����yP��m$��i w5)������p���8p�o�����9��s�7����k�������>��B�V��=z�O���sT<z�^T�V�?5?k�|
:���oF�J.����j���f���������<����^�4��_��������ZL]�����}���=�3�}�h�������w/,�o��Zw1��k���lJ�;^��=x�a}���Bif;�u5������A�|�rN��D(�y*_��	M�\���%o�xE�\*��
���{���!�^�K^y�z���s)�
��p2�*y���`v�����������	�J-c~�]}.cz!�/V�����]s����J6��buk3^m�)��G���q��
���Xg!�a�z7$:	m�[�A��cA�m��#�E*vIz1<��p�-���T�����ZA�E�=��('�WC�`Ej`�CIV��I���M�fT��]�o�:�{��X��t���E��!9��d��&�������K��L
��_�{�Rx�g�o���^���Wa�4��E�t�w6���q�w����M�������ZJ�0pM:���s*��-5'�U=��-�������Tn���'���SQ����'���+ EL:�+�b�c���Id��tS�����}����R�VsU����@V��S��{mP�����������#�`��+JcLwed���OW_��K��f�\�v�����-C\�j�%&�����V�C����;��O&��VHF��-jS�B��pQ�==j</�c���b�����k)�iXV���[_~����]�4=yY�j��U����U�=V�T<yY�r�����<�w�Z���|���2�V>I��|fjG��������G�Y�T{��%{���7'�C���������a��9��UY�i'�5�j�J|9��9c�VI\�&��O^�*��Egz�����ZlM���}��:�O+T��U=�X�e����S��T��}�Kq�;�l����y9A�f9/������[���ei����?(Wu
�J��C}�������Z�����U�QmY���-Z�.�W�����)�� }�e�J��
��V�5~�����N4��m$yT<��'�����C�����'�j��$��,�S7���8��>���'��Q����o��A�M���86�Sj����.U%��RH[�W�����J����N�XUVu��{�?nlMqwK�F��P��[X:w����;��S��j,�u����v�_|�rSqP�V<���'�T�����U���}�vf� 2�0����
�X�z��K��LE�Yq�/N����b�4����"!xd�<�z�\���Oj<��E5�1�E�]u��t�U!��p�MJ�4�
��P������{j��^;=��&�����#�"���o�����E���U�����������<!��{�]9���[Yq���ns�N�u
��V��xF/)*��W��_�=���p��c����<�b���[0c�Z����k$�M]��O�	��H#��Q�`����IMJh��gj�*����Pj��o�W���L��!��I
Z�S8�2N��.���I�����#3��a,��Xkm�9��F�x�����CfW�X5;_]��{5�����z�e?b�^X�����Y��(���n,��_[bb��W}r�5�R���U�����oAs��� f7	='=�������q����.���}��W�e�
fqO:#}����a�k��s��BF8H��,�j�b6=�#hf���������������A��E9>�B��}�O[Vw�Xsj�-���:w�*���c�m��������8H����wQ���g��s�[����o�����@�mX���yS��;�X>����gUXs�B�����������w����q�T�������C������/n��������4�}Y��<������u�gw+/Z���H��@���xM���������Q���1��57�8��<R���{�����������P������Z���<j�yyP�UmY������O�������%���O{�.0�D~.�>5�d���z/8cv���I�%|��T���<��-U�{�O���9V}��%}�=�=�n��*�:MI�gM{�!���c�	��U��Sx�&��z���yXw5|z�=���}U��>��H
��{
�]�l�,�F9J�����p
"��g�(s|�cW5=����[����"���T����������Qj���{���������;��w(_��
������q��uQ���N��|��?k����7�����9�yX{�T<UI�U���������n{���V3'������s�K/���{��N��v�����4:�*���6���~?����=��O^�r�y����j�����I��wI����&6�������&��;���D�^dZ�>�4x�~{����?Rr�[Vy�XwUa{���U'�U���=��N?��������R11<D	����#��>�����q3�F'�������R�]%�yJ�X�lk%\�������@s�t����U�%^��e�>}J���p�N�K�H��������V���-���eKI_�{�uT~��<Z�5����&�k8Y+9M������&w�G�w��v{����&#C����8�1�<������^������~�=)-�>�����,{����U4�����u���.��<�7z��}����f������;yyb�� dK�P���������J]s9�Axx}�)j-W[k�!��������w3�q�ou�����!*<`���i�f�T���������
�����@&����n����(�~9�G�j��n3W�������	�����X0��c�����`�-V������,��S*�p��V�p�j�b�^"0�@k�'�uw�����������FH�Pb����n�?��)��X��m0�������hlK�Zo7U��7$=�T������{�`�V6���|&����9�Y]�r���)�n���N����~��g?N����NE�b���g]��G�2��$�"��yP<�����+����?|��}����7\C�Ok]a�����1�Nt���C�w�
�B#u�:�����~��^T�j�yXW���U
)9>�[��6��v���.��+�L>��;Hb�bk�T����|+v���>|7������j���/�Vy��=�����z��Ud�����~�>��ZR&��SUU(�9��~��������s������)�v�uPU��������;����U���Ud��4I�$*I��XA<��:��|����]0]���l���-
zy���������z�g��PkT;�V
�/Z������gUYO���o������jj���v��u�������p#$��Y��W,��v����&�����mT}�Oj�<�V
NO��O����]�^M�[X������y��������	�����z��7���������y����d��T�+'z�/Z����y�++��������x������xuh�W�r�+��fo��47:nG� 5ej�����k:Au^�'0�$��N�T���=��}�`ymP]�@���u3�c�a
���Qib�����0��"��y~jb�2�����me��'uT��P�U���UC�I5V	&�W�d���o}b���+���������Z�ew�{nb���r�1�jS�zq� ���yj��R[�}���5/	'UeJ�{�jC*�W{�z���,/x�B��wlZ���G?n�X����r"~��)���{���f������"�[��o��Y��"�U@�x�9<������F�Z���m};8����
gl��d����[T�v>5Y7mx{�7.����q���RJ\M�=�T��Dy���s�zU��2��D;�z)k��h4��-Y
��f�MMZ�K5V(1��>����5����Y��_�=IQ��a��B���
�*?VK�{]�E
��s�P�wJoK��St�T�;c��V$]u�-�Z��t+e<s�������.��O����}�o�����L��w�R������n&No3N��R(w�����mr������w���!yX���E�W������KH9L��������}�m�.�:Gn��*A�����%��g�+yV6�y�Xw���h�=5d��hV
��x��U��:$��d��~�Vn�����4;vz6������Q���r���T�%����nJ����/�)^8g��T�������x,�{]������J|~7��J�`6�;�R<�!��H�T��U��Y�j�_���:�������il�;�@q����zX4������P�;���ZU��$��NJ���}�d}���������X��}"��|��[��^��]���B���-t�"l�V��CQ���&�wj�����_/U����z�g�������ny]���eq��U��U���6�l�����5wW�x��$�r�=���Z�w+<�����.I'���H����d'�<��������6��bu�n���eo��b���������<����'��d��@[T=��;�^��
�I������|���s�����^����i.@g[��}�a}�%��e9@��UF��MG'�$����^���>���������")^��t�7�1����������z�y��Z������m���$���H�UXsU�����j���A�$��i�C�����{�]��^�����+��I��N�pP��N�2�o�.���|~��
�~X=��z��V-T=�Y<UC���^Vz���*���V�!{F!���J�0���U�������g1�v��U�����������}T�,�����V�13k��p,
5e���(�a�^fm���j��3�/S�X���:)n�jg���/8*t���#W�~���I"�s*��Q�)�<�~���5��!����V<�C
F��u��_
s='1��mAc���3p��"��u ��jZU���i�{�T$n)��F���=�r��~�;���������=�-�Y��M�R������~[0q�r���W$Y|c�e������������A�����]r<����S7�|��t�kv57�����L�F]�wi����}aB������0�t�"��Is��Y���&c��������y�V ��u59
����*�����d����>�o�_<����;�����r�'���wY*C�Vx��j����w�;���6�a�fwoU��V\�_�G��j5,/u�W��]�����{j���;���P��C���h�O�0��~��=`y3���=�R\
��t�r$�P�����2L
�M��Z3K��n��PF�_��]���T�mPz���F����Xy����@y��9{��MZ����ly�W:a];<]�o���M���:�:~_���V�*=U�Ud��Iz�}JG%&��|[�|M�^��g��h��v�_md��R��N<���U�B��HmH.�b���NJ��>���9������
���U���������n���^^��I�y�+:,3�U��|X!S�����^�m���%��Y�5PkV{U��H��RO�)$�m����H�s�7�fSnV�^/)�e!��������M;�$�|���������yPj��mX�PmV{j�w5A����mY6���}�����9��F���*������_���7��t:�A��+iK��JI|�$����U
�d��Aj���o+=���g�g��?^b�z��W:�7����XV��b���k:i�H�������j������������T��>�I>K���w�s%�0o-\�'4�����4�{)j�i�����//X��W]��n�#mY�j���j����rO�rO�r9>m9*�����"�����809����
�%���*�`�f����a7��N��M��(��@�%}^^T����N�T�{U;��;�������X���e�����g�U�N����hT����j��������>���W���c��.�Z����M�|�������)DX�PygI�O��{�3���y�&�C��p�G?zN�qK�h�eT�7s���UV�#[��������xt�pu+�`z�����S�u[ui��z(S���kb:�{��tw���T��7�S�.%QV��k��{��\���nr��fX��(�]
j��`���}wL\l%Tc{���K0c�qx�s��nG��z��\.��h����@��iu��JwR�5����K���L�l���:�H��u�^:(.��n*��a�dD�.�1��
�r���pr���`�J�^�W�zgr�F�5Fc���/1W:��.��d���{��V��v�%���Y1/L�����V�w�[ �-�BeJ��9��n7Qgd�:�zp2s��A���R ����>���g(�vi�B��v��qh�!� �iu���pt�{�+s�^�VC�_}��m���������j��W�1 0�$o��2�E����hr���'&q�g�����)gIY
�`��������sT/U`y�RV���-�������B������_����~[��-�\����n�8k���MB��6n��Ao�w�
rT�I�{��yRymP�����^����U��Z'C��������*jW��g���^C�1T�+�L�mI�6���������%��������;��:������RO���,��}� ;�$���y�,$D,_�m��>w�	�k�.����q����J����F��US�yP����yX{���VO;���
�N;����o�a���Z��j��H�	7�f�����x,�����X��x�C�j���_mP��T���/�y�X�/G���^hfs3����$m���������|�d&�����������mY;�����+_�M�%Qo�7����z���8�����oX�vJS�x��w�h�����3���#�rEF��k�X����HI ��S�UO[V�S�U`�U�������{h:��)��K$P���z����qFJ7o���WVi�%�A�'�������U!����*y�T7%}H�%E��,����/C�����M����bp���-�e���h@{��y����/�w}���:���j��U���������X{j�~/������8��nf��HFlb�W6�s�V}��Lmfl{T�z���b�r�x�r��#6���U�5Dp��F�wj����P���czd��Z�iC�����0v"��#���B���s���/'�[n�����1�5Z5z�3%�����e�z;����Df����x
����������8�n�%HZ���I�������j������5SO"��`x��4�����$/xK7Z�������%�F��l{�63R<�i�{Ya:��JfZr�(s�gD��������1��KW`��IV�R��y #/C��$�6R�2�����x��Ce�SonZ�/�$�8(�iH��58���k�;�c;^����6��MI�����-1�u�U�C� dp$������d�����VK�p��?K�����
��w^�T�V2����tIx6��n�1P�J��������%�c����J�V���4WK��(�b��m�����������&�^;i�%�U#:>�w�������aV��j��T<z�;����
�yS���q��������]�"����y�X���gV��n�^i^[�[���qL���N����U��R{���UI�*�����I_8���NIU��m�m������Ek
v�:��f
��]��qsyo��w{k����y�'^T�=�a����'�T'��5Ry������s��%�o{LA3���q��z�g��
��������	7�����I�����UkW��=�Q�Ry�T9�E�~����&.o�d�-�p��.�������	4g���i�t���Mv�V�T�/+����VmO��$���$���M��T�,.�p\���e5.m��=�X�q�?*���_9������j�-�=��O=����T;�VN�T-�d��W�Y���wo�������r��w+�����F�Zd�+����Y��[������
^a=�|��U(���j�yS�V5/uO:�J�_{J��'�L�+��&3c�V\�K�U�T��	w����N3el���Q7��/���=VsU�j���-U'��(4��r}qSC�C=�}^���V�2i
��@��/T����~q�s��R~���������w��V����m��U�{���*O����?��"��������z��v���K����^��v���hWbY�A���>�p�W��URyyY���j���J���b@^6B�'/V���Z�6�j���r2hu�y��(�\Fk�����q�S�Y�q�OnfP^��%�Y����XWL��o�R����O�����ya�r��v%os�~�:���\MT��c�s4��	B���]���b��OV�y
�2���,�cve�����
�����������xP�t�W5dJpW�<+*8���I�\�%�{�f�����{�0z��w4�<<��
"�7s��a�Z�;������(���S5������/!�7�n���x�h��O��k�z�K�|P��HA�.�Q�_:��B���N�{�}�����F�^���d�1��z[��WN�z�G
���BC���5�n���m5�=R�SU��[>ZG��A�V����s��bw�V�v� �����e��r��3���~d�N�+����K�,���~�p9��Zr���b�{L'���+���[�ut���#�.���79g$]���%#�!��AU~����"�B����7�~}����������Y=�����%�*�NO��$�%$�>��L�zz��q-�w]'���Z��*�����%
�x'�f'��X����F������$��U����TUg=V�`^\�}��z��\$>k����y���M�	ko��y�v��z��t��#�aa1����V5T�[�����*���v��9�iC�;LC�������"6��\Tm`=����$Q���9���r�$ki<�=�>����*�U�hb��b���]v��/.��+��!�;�k]� ��_����Z�J����#�v����K��������"�����/����'.��!�������m��=v�LC;m1��1Z����LC��'v�~��<����sBtH�Nw~��9��i[������b��\f���:�lC���nv�!mvB�v� ��Ce�1v�����_W$:~�W��~���7Q��<�Q���\x��h�'o�k0_����c���}���>??��s��/]��=��LC��b�lA�lLC�;�&��`�6N`<1h�q����F�88Z]���`���{n^=�Mc��~|��v��j��]�C�]�]��7;����.]��v�����g3=���.�K)|	��1	�-�f-���Z�w��c�t^��b���_�?;u��O�������uv����1vv��6v��wWh`���������`�b�������VW���{qRD�!ll��L�^M	�:+���J���������?�g;���1��LA�lb�������!��LAn�1
n���������-�����"��{t�B�z!���Dwt��f�}��w�%�9}������+�QV0�4���{�t�*�������������(��|��[h����@��/�vj�-����j"SD�fK.q���uwK�M��9x���8���#x�j���z�h���0���o��%�bb9+u�VM��=���ns1������0����y�{�JbM��Nuk�����t�<�7V��7�&����������v���1���G@{�kX����i�n���>7mF#�����R�-/��(�[Z��v�g�(w�V^�}������xW����xs��wRK%��1������d
�H�Q����#NcS\�I��:5�nY}2�d1n����P��4�7}.��W|���k:��)n�����g�z��g�]*���
��G9^r
ac�����hn�����`����4��Q=�V�a�Z3����3tr���c�x.5����]rf�;��k�#[�W���J�W�3RHw�\�Z�1�lC��Hy�\�e��!�v�!����������~���c����o�i$��3E����=�03]��������&��|<2�f����1��b����v�b��!�v��-v�!]��>~_��z��}�����}p�x��G��`����O�����-F�}��^����y�LB��LB��1v�LC������b��1���!Z�����K������]��lY�s�����WSf�������)�lm���??�������!u��<��m��ghb��z����<	��=��n�g�Q��)���S�������<�����Yy~�Jss/e]�{�9f���Z����������[���i1
�i1\�C��hb�bbm�����!��~����C(f���]Q^�7UWtI��v��9��=K�^j�!�t�0�TW$�`~
k�1���s]��m]�!�]�!��!�;c���m����kG/W�~��>������VG_B������C���"�dwMe��y��V-��C��!�v1
�����m&!�v���lLC[�&!��i�Z�����3����xsMo��a�
����������;2!������|������!�v��s�&!��1��!v�!�v��6��A�y��g]�����[����}��m�)K�����w������X�&c���>��C[�C[��.]�!y�C�m�B��&!�m��;�`��f"�R���Yu���g�f~�[a�5�x���w���*nU�#�N%������}����������?�[���u���9�lC��A�������v�hb�l ��b!�������.��[����4�_�<i�-��v���`"����vh 1S�l���1������m�CE��{�	�4F�[1�C
e�0�0���������TUQ2����������ED������;>LAVa���UY�D�*(.���]{��VUX{TgY���TAE8���������W�gSK�7�����js��������@��77���rc��<�>���}�	>��}}+����e�u"#:�_!��7���}���y�����T�A*(#
������aaVb�:�N�y�0�����>�7~��|9��EQa6s������ED�]���{�7S�GF+���SfBAW~���"��u�����>�3��reQTaaQ�QXF�A�ZE���e���x/��`^��*���(M�y{�y�����\�S�iR	:��1��R��	s'�^���G:����7��jm���i�$��� �^��w�<(�0��n�����Q���Ew�����B"(�0)����nV?{�S���a�Qav{����*
,1mog�g=<������DC��	�����,* �M��L�� QW�d�����V���l�����
�h��9���;*`8��U�s� �Q��xc�88��)�bA�3��Z��L��R��7u8�`'���4;�p�}u���EkZv25�=�&k�sS������g�#
��;��K��LT ����o=��U���("���o.���n���TDQ�:�#VRT����\�pj��#�0{��������F��5��aV_�9���g(���frs�.��H�K�z��M\\]n��wn��ml��{�'#}�'>�}-�����|}� �!����zh3^<�l
z�x�����x��}~���'ORE��[�0�
��:���f
+���)��*���XXDR'�<��d��TAQ��'����M��i���E���]�n�7���aXUT]�}�S��wI>����(���g�>~h���-���i����y�.�q��-
wsB��'����i�����v�v��B�XO*���Mf�@�V�9�n�
R��'v��`���T�!����'�v�6jCNm�7[	C(R(����*_�������V;�M�.����, �MV�}<���DQ�}���Ol�L��������Ur���W���sq��EAa���<��c���
��(�7v�����(�q*��c����d��F��k�/��^��7W��Eis����)Ms�,��4���4�l�?~%���d8���k*eq�AVs��$t�Pr�/�w�z�����9S�|��
�����]Z���]}�������"��{}�3�G�XDQTPXaEE��������VS��s��?yc���>w���L=����O��7uw5� �,*0�zb���������Ku��t\+[N,��!"�C�W�u���P��#�mQsu����FR�'��/Q�&�aI�2�xgh����z������: �@���!�������Mb	����32����v�%`TE�N��c�(�
*)��w����8�*�*�0�,�����U�aF{��ot���	
"*C��k.x?p�AXQaE��������*n3���V`a�y��O���l�xMo
�xd�f�����(���F -W�H�7���{�.�����w,*������Z�l�wx�W���������9�nfZ]��y���>9��e����nK��*�G_l�(��*s�{��qTDH}��no,��� �����;�������(�C�*{�w#DXEE�o��o���g�����������Uk
�������/f��.u<��{q|���aj,����5����� ]��1�s�Ft�����������;�]��-�Y|�c
����!��t���s������wwE��@���WS�0��������DD+��'�����0,��=[u&���9�~����?m"pO}����B����,*�"���h\=��c
�]��si��<n��2Z���n]P�c*����#^�zj�Q�e��;�5����
W`���]{~|��.�p��n� �2Nu
����fQ�	��m�J�A[E��z�)�_M�=:��\� ������VQTU���o��n�(�����c���99[��,Pa�Ea�w�������FT`Q��{��9���VUa�}��lE`EXF�����0�*0��l��^����*����Q����q���|�q��%^�����R.���[Y��\X�0t��S�n�|���P2mf�S�m�m.|���u�eF���S�������C^�aVP��=�6ETa��/�w�����*
����y3V�1��7�����,*"=~��xe��!y5�g������YO�KQ���
&l�3��/�L#
0��V���tl�1Xe�e��310rF:,�y�
6x2czAkP!�5��7�O%�YJvI����M"���6�6yr�>���;�y9�%T��V&�������=c�)���}�����#�U������*"�<����5FQS"_m�Tj*#��oyS���zf��~���UEEAUaG��Y�}��.XU�P��UN��n��h��r���;�m+�����C��'����k����*�* �-�]��������k77��=}���U��^�7�(aQ�B���vm'���p��;W�9�D�=��UUXADU@PE�����������>����|�VEVQF���Ofy�����oy��j���S��3�"����",#�3j�j$���}<^o2������"�� �0(�E�-�����v�~��K5�U�(���(�(�>>y�&��
�U�kM��'���7�\��no;�m�EaA��w����.V���a,�����s�����UTa�aPR��c�[C��]���&^�_I��q�^��z��ie����^3X$�W�J��}Z��������������7���][7/����
D��6l�Mn�DPA~���w�}UGQUD����N�"��(����\�������re�V0��(���~�yY'%�oLHaR���[li
�*�����'j�,XDPa#���eg���fn�rB�����I"g�[��nC��q���U�QaXEUPa�r��"g��N3����y��k�i���7m�9J�0$*����r��gn;Q��r��r�]&���;�!PF&�������f'�!��"P��:�2[g�AQEEaX`��^�_��5������y����Q�E��-�z�$��TEre���������RXu�{�z���_7�\3�T���'����;E`Qaaa[�����Vw��|��2�v�5�k�QU�XVa����Uy;�u�77���u���-����DEEQEz�����v���{�f{�wt��z���^��_2�������b��[So�����g��+��xQ�
��n�9�9��z����7*��W�M	��#�`c|j4�VF�j���p}��a9�?dw��sQTEED���I���,"�"�������Z"�����;��{��EFA�E���l��!ADE�g�����*����	������o	EEE[������{����L��"	
�"*�}'f�����s����W�W�� "��*0���|(�S0�U�]���
�
&
�u'QaU^\����8�����9w���)���tU�AXQDo��z��s���m��^�v�r������P"�,*",B��)*�<un�>e��r��K6��},,**
��(�oz(��M�
s�����n	v:�p�H*(��
C
�#����=yvC��-(�
U�.�n�$s\�����",
���{����;����yg�'���8w�7��aQXEV*�����Z/%��5���0���yh�XPVDbDotn��}�w�*��d���w�&�m02��T�����$�Ws�h���X7��T�Z�	���Xb��V/W5��������@�w��<5�����X���=�{�^��
�������������k*��,���Mt�X�QQc}\���O��;*�(���
��V�H**�����w�����PaQaD�������*
������j�(�����s�����|_B)�RAW���3~�'�{'L�������w3�����V�C�������<5��]�,��N�N��d�j�����(��0�M&sq��y���oQ�;Z��X��T�`�{���DUaEQT��V�Wr�{w~�r�����2�l�5D`��"���T>�����n�A��U<���`B1����aUA�aE�^znM�Q�$t�K���tf�s4s���@�
(������������w�����jJ�����k�r��XXEE�(B��
�Y@��b�A�{er{=�x�p��"
0�0���]K��8l=����}Q�v��T���p�/��7�os�.Y��6��,N����Q�~*<�Mw�b�s�7J��4���5�����{L�[����2��[ry�<ty��S�o��g�N��"gmW'L�BY���<Dz aQQB���L���TUV�_��w��9S�s��E/�3/�5�u�(������r����`�����f��/}�Ug&ITF'��}�b�����w��c��C�����{s��l��Aa�FPE�UQUW3����}���6��ke��2�K��EXaPAT��CIC;p�����e����m,��`xTUE�AQE�z�{{�������|���v{�{m������"(��C�7���^V������8\��/8�z(����
� ��-Pb�BR���A_Z�o������:�"0�(����8^�������o����S]���� ��?}uZG=��|n"�}S�t�o���yXUHUXa�Uv�M�:��M���e�;�_3m������""�
�"E��������+�/������=7D���TEDUXXAT����9>�vC
�H���o=c����^����v+����O%�}(t�C[���>+���=)�J���K�82��S��?	6�������|P�_�p�D�*�
+)��^�����#�
���;���Q����r~��H����*�Oo���J0(U�!�y�i��>�s��������JQ#
��������U��XUT8��fy�(��P����
 �������4�'�g����7�j�<��]����qE�A�a�����x�w�f���i�}st[{\��
�"�(������6gp��y��y=�S}�y�E�UFTT�������ow����L��[�{�����,0RQaX`Q���gq�����t�27a�E�I�;*�*"�C�V��9'=�'Y��o�}��N�����'�=��`���(�����h�^w��g�{]��U��1�zs���FUXT�����1��*W����nc3z�i���,1
#"0�� �:�y�����u�2��c���/U��������p��
v�y�_��s�/��"�V�V�:�<����^myn��V`�]31�vr pq���/�=����!�`�6���Tu�4�������	BJ�������q���>�����EE�>>v�=�/�s���EU�����g}���*���'\�}��DUTT}��s2��j
0��_e�k�|���;
(�(+��������\�,"�Or;f,*1	��]�r��9��0Eb�FEQEQW���E	����SB��=@(�W&�L�*�#�'9������_��,.���9�����N���S������XU�DTb��������f��{�U��[^g�>��s���XAU`a��s���k��8M_=Y�v�^�tw��0�0���F�)�U�+�L�����w=3�]�&g�w
�"(���*��#UOm^=�'��s�c�����y5}��|u�AQA������[t�qz.���Mb���L�@UQUa�!U7�7�t��'����y9�����4���TQU``DUQQEW{�e��4z{��$�Y�V�����fz��{��9�UXAa��EEE=x�f�u����U�����j����v����Ev�]��85��������tNJ��\q<���9��a7{��n��t�[��]Q�i�
�}���k�J���jg�����|����2.UAPEQO����x|3^��XQaA]N}���2��Qa\���6�UI=��O��y��F`�>��|w}x�;��XUTA����x��
�
%}���,**�5Z��~��g��EaUVQ'�������;�xKgN)���Y��*������������dy�{��u�e>������g��*$"(����*
��s�~�6�u����^<'�Y����T�UXQ�FE-��(�
��=T2*��ma8�*
#
����S��������&�^iS��~����<��������"�O����S4k��{��S��z�w7�}���DXEEaU`a~���� �:a���<uM�F��=Rh�0� �� �,O'�D�����9��^�e��=�u`XVUVG��n����Zj�5���f���r���f��%�HUaPcwl������`�)y��r��}%z%���W��U(�E+�tq:=o7|���}^,�4`�C��b�Gd�������C��CV�������S�^��m���O�L.������
�����(�����7�����0��"
�����m��0yEA�ky��p�0��, ������<������EDQ�U�y��;�s'���������>o��/�����QDE��/�����4uQ�!V3Rj2~����z!_������=��Z�fm@K�;_o&��VYw�xv0�O31��SODk�mU������Be=�����n�3
���d������LDgQ��y�\�2�����v�Fj7�m�\{�z�m.u;x�PP���,!SJQL:vvm����4��d�sJ�.\fL�l�u�6[�T{
�M����s�J�)f�|�t�4,p���*9���SBxWH�f�����7]<���\A:�-J�>���V��_e#}��]X�yu���i���81�;����5��:���%����he��N��x����W�',��#E$*�x��A���Dy��6�A���.�`VEj��v�`���B�*�;Ej��Kp�L����Z��(m:p���-���YN��s/�Jp�����_"�W0�^�0�����amk�iD$�{�wy�7�9SK�j�\��o'M��W%�T����{�k��9�=xR��=�1��j�M]���7�:ve��2e�g���=�-g�
�����dX)��"5��[]E������&��YoD��vb�����Wk��d[u��\)W}�:����9G-�������j�:8�z�{�8���u�����dQ,�!S�_]�:�
&�h�����uW;a+m�V����Q��k�I����7ma��}0\��������79�K�H]c�C�����;�������e������R�(��/���.���a�����\��
�h���V�,�����GT�fq��./�IH
i2=������$����\�k�q-������a
9����������;N]f�wn�������D2�d���,q��3&��n�
;��Z���V�\�u��'�X�p�{3j���n�S����<._9���e3Y���V;����%	4N|�hq���qg���p]t�jNM��[Iuu�F�b�}����������\��4�)�������%���t�X������7{.�RZ3i*X�7���r�K
�.����,���hV�iY\u������M�����8���y�9�D�<�Y
��y>��?wlv�n@�l7���H������C�xP2��5������m�ci���w�m+u�!lwn����9/��_q��T<�bx�n�a��7#�m+��W����9|�py�Q�s��S����+��s���~��jY�mRY{��7����z_Q�N�x<jC�����w��DwmH:�Y������"��^<b2�^��.]gvh��r��+u��7��<���������hKi��5/0T��i�v�F�J�����{�f�������IXc��Cp�.���T�=�[�/<�qrr��	���B�]�	�������;|��^���eA�����'te��GCq��K�U\�Yt�]��[x��k��c�6g(�vC������������w��]��Bz�D�������[���6�"cS0��	t�s�N���n�e���2��`��R3�Y��T�;i4�L�s*8�R���!�^[Tg�6���oN����Bv��f���@�x�����r�t�J�kz'
m�������9U��\�������:f,]�^5�8�Y[�'��/S�k���2t��)���M���l4�"�R��g *����k�}s)�t|c��q�.�F���k�+T7X��n�w.����qkm�PI������A�"gS�j{��������}�m��N�%�b9{|��#�3����
�C�T��uFB�@��2�������
�&�2��I�4H�8��%:j�U�)�	3��D�3�)�����[�
$m���g]��y���4�q��6�>H����h}�o���SB<����2�+/"���-�;�Y�.CFm�a��nV���fA�r���p��@�yn�M;���ev8�(����S�������4.���Z*�Dl�W|����|(��m[��6b�C<�������������{�-�L����l��_&�����RJ�N��wQj����7�eNf�ZQ]j��S�w�j���K��%,��
����Xx��k���)4
�W������`��U����G��
a��	����j�G����_oUyt?xG��!��1�x��5�`P|�	�B����]��,�M/���6�D�mc�6�UJP+MM�0�Cq])^0,{�%}c��cG���t�^�3K��;#2tLG�4�{�����pv���&Fh�^�����-�{k���c���Z,�j����M�+���{/z�z*4rUcp�������nh����������<UmW\T����\���{����R�����<V]��pr+2�C�iw�]4����V]cTe�k��,��Z8��)��Y��.��!��2��&}"��Hr��^����8�YOY��e:���]�),S���^�v���&�&��
��v
���]�o�Y^�
x�^��j&�VR���4P�c�:��Z�u)����I�]���.�vvP"�x]���R���>il���U�,E5Fi����h����#=6n�jc��et�%����S6�V'�$��Z��,=��+3/N��������!z���:�i�{�����b���[v�������m5}G����~J��~�3�8���
fr�������E��I�'�F����!^�~wk��vB��&!m�!��b���9���]v��{s��<*�3�xk";|�������{5��#g�4#��*�=����c�%������?�|��y���@� ��& ��&!mv�����/g`1��C�fg�����g�
�s�xbe��8���`�?�v	�b��f��zm`���v)m�f}q�
�L��N{��'`<{�hb��1.��k��.v�������b��b����?}����E;t-�����5���/������q�{I�wn�o9�_���E*�C�����lB�l���������+��!���b���?������^w<U�o���Py��d3���aj�e
�����v(7��	�����W��I�Z�!�]��r��
���7]������t��vvJ~�[M��d������G�8�������GU����EL�X���{�;	����Cs�&!�v�Wm&!y�LA�m&!yv�!���=]������kR*�?�{:�EI��d[�]�u?V�@��
J�qOD5W�������7����bv��;m����&!����v�i�9���3�<3/3O��N�����t���Q��s�����]��R�0���T����<��������b��1
]�1
s�� ���!��C����wm�1���?�o~����������� h�2��u\9iXu?+k7�fh��w1=�7����dn���xY'0`��v��nv��5���{�l���I�z��1�mC��I�=���f"��O�n���!,@���y�����a����b��_3����)\D��nX�7��1����|����Ho���1�����U����}���{IG�*��^S�#�����u\l�{\2���j]mu�xu�Nf���u�1�qa��TH�G#�m��z:�<�����>�w7�
�U�@
��
�(�L^p�����!gzkG�[�r���h8^}U��Et��������}�'ggh.�����������u:�g/x
|��u�g�)]	Z���y{Axv���I�������C
pzd���[@�y�	�3E*�;���yX�n�������%���U���SHp�{���X1=��I1C-Ujw�gL��P�W7������V��]�������v��V-���+������oi��_��,�K����Z����{��g��4,��\)H*������iu�c���hG���3�]*:�up����0t!y�J^Te{�G1�����I�k���*��Uv�!yvB��&!y�C�������������0:����@�Q�3[��Q�oo�W!W�R]I�f��~������A��C�vHyyqC�v����bs�b���owv4�77v4�����?3�ao�}�sW b��[�y��o;�^�����?;'g���n�CU�b�lbWhb��bn�b�`�5�hb��1������{_��e�
��J��2��v0c,���j��me%�y������'�d�����V-�1n���m&!�m�1���/ghb]��{���.���62ve������d;�_mI���Fi���$!Y���
�-���-y�~!�����LC��C��i�u��r������C���U�F�;�_�x���J�QU��}��i���S���������g�V��O���1yv1��b���]�b��b���!��a���th�r��~���,���RM�r�A',
;�K�h�:yN��S��7���w����CWmC������!�Wbb�`�=��1�����������	�&y���?��'&�X������R<��#a��)	k���&�,�Js' P���e^�|<&�f{�<*�h��v&!���!�v��;gi1
����v�B�lLC�����������'���r�t�Y����.l�ch�����-����v�JO��B����m��n�i�r���;;!�;b]�C�]����W�#u�����I�K�j�j}�g�dU�I����r��I�),��(I��f���O�v��]��3v�b
v��:��1��LA��1����swt4��O�����o���r���}ZF|=��Rs0����E;�u�
��j���x�v&R��W�p������J��dX����;��u6=�S�t��h�p�aws3/ �v����,;�����#�����Z�d�>�Z�j=�5����B`]=��`���6n9���vuhf���x��>���W�����K��������m�����9����q:�]ev�3+���=q�d�vzb�����l\ee�x��Z�����o�aZ�11� J,���
)���7���t�f=�z������6-#Y���uP5��q��8�G�.�����tc�Ef�����;\��`�m�r�dl�L�fJQ����
�b���e������b�>n�����<�q��[�f��V���l��
���'�m,Ki�K��t/��5���K{S1B���{n���FY��a��(�+V��k"���1�b2��D��]���T���b�+]C�i�kl3���e��m���u��=�k�����,�(�Y�r}���m�!�;!�� ��LAWb��l!��lC���Wn�4|���:�7�(��O�+��'b}��?�K����	���6
�mqf���k���;I�:��5����i�s���g;����=8s��+��*����}H�yqOw��GsTn:�����k��m:Jc�?���O�����?�z�bb��Ak�b�i1s�1��b�`�����+RJ��b@O���w�5���mL�5���e�l�������^�v���7�e�}~?�?�^���;.�!�;�k�b
������-��H377`�	���4��}���G~�3rA_��y�Yf��]�4����e���|��o�>{�����m~�]�AW`�5������5v��;m����������A���i��<�w���Z�1��A�r��]K��LJ�����]�\O`�z7(f�3���n$��b�`1�h�j�B������� ��]�1s��i�����������)����:������S������}�6��wp����������]��=����7lLC���-�LC�v& ��&<0�f{�9�<�:+�&���U�M��S]�{�N8OGc4GK�<��_�Z����C����!�.�b���5v��^]�n��]�����`�$�g�
���_�����[]�����h��)�(��l����,&������|o������
k�����@�2���u��b��b������C��bb�l�����������T��\�sI�9U%���<�Uc�����;�������]�
�bb��b�v&!{;C�� ��bW`�=��1�lC�|�o�S{��u�kFr���/�>�0^E
�������s0������l����GV��k�ch>���9��N:��Yg0Y��v����^�g�7q����U�Fw�}lU���x_���QH��������M�\�Z�T��y[���\EF�R��T:�0<}`��\/����2�'Uld��\�=���ll��o~
&�kr)������M�����j���ZXT3�f�����"c.f.m�B��l@����t��8�S=���!,>;�X�����l�1i���*
�Y�g2X�t���|�B���]"�W]�k�g6�Ckc{��Z5?q�FS4��&�}�C���$6����"��������#��'[V����2f����u���!	4�Y�,�:�VE
�w$�|��S��9@z=��uu#������������`o������(�ESV��ooZ��Gn\��{�znI��B=w��z�����1���&�'_���n'b�9�U��W�oMJ����LB�vC��&!�;!����;]��sv��*�CUa�����,W}F~��/�>�T����]S���_�ECf�{���Y��eZ����C.�1
����m& ��!�����LC���!w��x]�s����Gu_J������Z9�)�7��������z�U�
��"���������?�l�������LB��b]��=]�1��1v��{��9�`��Z%�o��=��7�^��T���F�~������[�#����������z�����!��v��v����1;li�f��U�z�(���������� ���C�����I��#u�8+�����o{�I�As��m�& ��1s�B�l!z�bW`����g3<8���W���b:6����B+��_���j�m��tq�O�3k��b�n�W���>��@���b����������v�\�1[�&!��!�����z~��p��"���:���f<�I�PS�$5x���

����m���������c��������;m�A]�1
���;W`�=���W��h��~�c����YK�S�L��������]�����c~�/t�9�����]�1��LB��b�i�^�hbg`�v���Y9������9�����d��b��I�{��v���j��'���=�>���������=�����=�m&!����`�v����!���!v��^�0xe����_t��>u�4fo�5w��#��B?�����'����*
���M�
�+����	9���}vHyz��s�bm�b���7]�b�������|�������'AX��7bwN~����%&u�c�qv�K���b)�x
���G+Sn7�h}�o�;&(v��5=�����H��vk�{�I�{CF\f�4�Y������b�T!]=t�S��e�<HY���*:��X�����O:����;�,U�
�Em��w|>
��������x����O$�	�n�P���6n0^
h������F=5qUf�\�*_�������MoQ�}H�e
�y<��x,�����\�U�%�����u�u���*��UR��D���L���$c�[oL(*�p�Xtu���ex`�$@�Oj^/.���^��X�E/{/a��^D���n��Y�i���8o�s�CF\L�������������xe�vO"��`�&	�������R�H��b'�|������O3������/8W�������WEz/?���{�VD��������m��c}�7�9�<S�6��8n,T����VV�Z�����!��,���;*A��m^uzfv�ne�M�5O
)'?��j[�D;.�!��!�]�!���]���]]�Wm1�l!����~M�-����~kiX�]!��W�[�=V�������kH_p�2X]�����W`�+v�bv�b�`�5��bm��:����v��:�����^��!n&(��+�'����kk�_�3N���=���7��.�kz��~O����O���1
�i1nv�!��i1��1����!oY�����`����������n����t�}�&k���o�)"u�gF�x���/W���b���}�����k�����1m�I�l��!s���k���v�b����o�����y��g��M��Q��;z�����|+��-�h���
�{�_e���'%����bb��b���]��v]��Z�����1z��!��h�<��IS�~b������Y�������)�f�`~t�fj���"n��q>c����C�vC��C�;{m��vv�!���C�vJ������Zg���?�z"���b�?^
��D���_�80����2�!�YT?>y���9~|��� ����m]��z�`1�����1j�C���b]� ��&!����������R�(���^��:�O����	����r���*|�9*Q�.�qRW����]��;.��=�lbWiCj��������LA�l��l�s^����k�1�A��8M�����)�����������Be7�����5��C7lb��1�lAs�1s��=]��=n��s��*��E/�U�6����6�t�����������W�
���.�AV��`�z$�"���;��Vg��������C��&!�����LC�v�!��1.��/;`�,!��?{���G��c�[�hz����K�?;&Y��Y5�R�]��v���c�;T
Gy|o���STu���k��t=|
CK6|)�I��[��� {H�v��W��jo�����q*�*�o�|���f�v;�����2�=x�=
+�G_%xB�}�}��&'q������.���%�(�|=�6�w�e��������EnJ1Q�-7�x	�M�U2\��T�c:�b��B�f���6����g���v��������A���(�`m�Q����6�
�B;'9�r'��^���(������n3�'�yg;���`���Y"���3���P+������EN=v���}�:+to�s�U�J��w7�:�F���C8��`�6�u�;���[Wr�7��y���i����E�WukNn������Wv�n���T����xiv=��������������r�+(�����$���d�V�C6�k�a��6q���,a{0�w���V3�zj��[s�Nv}��������v�v� ����\����1k��7Whb�����?)�?~���7��*���3{���M��g�^sf������	������t�1��1��1��b�m1
���=�l����e�0�+�����A��EV|�+��9fBj�W��/�����:w�'�����������~_�!�A�����bm��;W`1�lLC���!���C�v�
���4|~N~��
_�l�?&��
��K�����OP�O�w!����J���=���}����?�*�LC��!y�!�vC�����b����mC�]�!��}���������f�id�^���|�HM�2������B>���r��o�����o��G���b�c�v�!�v�1������6�!���!B��;�lvy������`���C_�h���������'�����ZO��	Z�!�T�"C��H-X$=���bB������������{��������:�si3S#�?j������l`��-��l4�I�OU~��u$��}Uv�b
��!����k���7m�!��LC5�1�i�����o`�~��K
�V�gv 2�<�z,�6�!^���D}�r��o{�W�N��/gi�g;@�5]��v�!yv����LA�m&!vvl������y-r����c��9`����xarN�@�i��P�:Z\����_R�mA���; ��!��&!��b�`�=��&!�m�|#��I�����o%��������U]��E�~Y�4��G�&d�o����
sku���>�'�j�CU��+��!��� �vC�����b��0xY�����a|+�_e�}����[������C�|��6i�u�0xw���V�������V\�S�k\nY8����0���,��T��{X��S�`�����;g{�����e%��i�N�^����G�3�j�q^����n���8[���P(z�6����}�}���}C$�a�����:���B�jr����fz6�R�fw���:�D��v��~���{�w��`�s���n)�}_s. /Z�^�&M���|2��_P��C�>j��x��10��6�����b1,aT��4:v�����3���p�Lt���3
59��]c9��EM��!�rj�yXA��)($��8��b9R�����1���xH%�aQ��<�1��r��X

��E�����f-J^<�4,���}6����5lFiF�
^����}�����4�]��(��	k�J������5M�������Uu������v�n����`a���L����<�mlq�����U���s^"7_��?�k���w.��;@�=�m1�l�\�1k�����0xU���	q
����Y�2�!]�Tt�L��o�L���N�	�Y�lT(�p���=���s���kwt4�6�t�
��b���e�C����Wm��	�9�`��N`<9��(J�a<s����]����rC�y4��������w���.��[v��=���.v��������m���:RJ���%�*��j{�/���/V\_��Cy	�CEtw�i~�\�qiMf�����g�6EU����$��	$��}^��LA]��;��b�`�3v��v��:��1����������}�~�hedB�������Z��*�Wn<��K�7�A���A����N����lbv��:�hbWo`�������o33���o�=�}�;�m:�>�!����m�W���.�8��5B���$�9����"�U��{��1u�LC����5���=���v��v��������L���?L��Sx&��%�3R3-��*<�i^n��\�k?����}Qghb]�\�LC����]�Z�&!��!z����R�����{�m����v����r�M�B��96~�9�jQ���P*�h�l!�;c�]��u���������LC���C��
_U�����N���i��yQ����g��V~.f
3��*��@�������D��!�m��;��b���9v��/]�1]�1U�C�v�����|�[���V`����z���7�F���okp�V��g6�s�$�W���1n�1�hbW`�+]�m�s��*���jK�_R\�IS����8��S,�����E����K{����V�W�����B���D��;�^zQj���;�;q+������>�3m3<8�
���{�u
��)�,xj��m���������1�Bt5���a�C�-��<5�\��P��E1���R�	���_W�|������p������������K��o������gm�^�B���q�� =�SYCls���=���;������q%��.��o��<�cy�r,��|��m�H������7=���w�hv<���{Vz�QAt�h/r�;ln��Ku[�9�Py�yw��pfC��U��x�s�h
R��F�%�t���>�b�-�x�g3s�1�5�gV1[�-�c0��V�0�]����1C��-u�k��YyXp=6�n����c��d����L��
����2�{"��O%�AV��4,��E�g�C�\cW�3<�{�>��u{����[�_=����Av�����=������1Wbb;i1\�&!�v�!������?������<���yYOM�#�k�r����_�!��g���e����N�3 �AW��i��1���6���m�B��LA��LA��A�l!����6��&�/�����:V��F`�����I%|��k��f������}[v����vC�v��n������.�cz���m�!{m�>���'�w������q����n������$�+�/.��e,O�;������!��!�]�C����vv���� �mCj�C�����uy�0xv����qG�BEg��y2���Wm��
;�*7u
_�M�W��������B��LB��LC�m��{v�b�����lLAn���hb��1�����z������/�k���������wQ�������8��O���?{����A�mC�v1{m��{��1j�1W`��i�6�!�.��=���~y�����+�����w[[s,r��T�}%��}�]
w�{�w�n�{9W���AW���U��b���v�`������sv��;]�1
]��q�}�����">=�"~w����+��}"��[��_.Q�������[C��1���|u]� ���[�B��b�bb
�c��!m����J��^��K�D
eO���c��qq���WJy������������nq���*��AWh���{m��n��yv�������m�R�G���~�����X�'�����CHf�;3?h��U_�LT�x�O��q�<�_�`�!��!m��{��LB��C���������mB�l�n��>���S���y���<���"�N�D���~�����)�����J���
���{v�TMe�����)���b:kX|�n0W�k�z$��@P��g����9c���nn��t
������c%L;������"R),�3v�z+���#t`p3���9go����V��xU��P6�zB��x]!'!�gv�-7�x{�^�DD�g�\{�
y��@wq�yx���|�U�y"��h��~���T�U<���C����^&���Jc�>�)��C��l����]���N��Fe+�e>�__9<�%1�~�W�o�o����q��m�
�f�����S�+��4`��3?Q�\�w��|��Lq��V���<oxMn����<.��M����L�W;�I��=\	Z��s>�����D�S�������X�(�<7��_�3=�������G�|P.P/�v
��,y� �Z��gm`��U��<f�>�S�g:�{�%����F-2E��������������`�;;i1���z���l�LAk��m��d��`������G@�>��Y��u��n'7���5(~%^���z6Pm����]��;�����>~~��
�@�7;$<�]&!��I�n���]�C.��k�b������������w�qe,���V���]��o�z����u1�pV�Z|+����|
Kt}���n� ����������k�C��A��1
��LC������V�{c����MM��v&���TaJ���kM���v%��-��{{����v#Hnv��3��!�v��.�b�i1�����C�v� ���>Yj�_��R����,QW���|�������FW�xNM����wY���C���`�+���=�m&!Z�LB�����b�l��m1�ts0{�1s�+��y�������������_/@����E�S@�l�%��)O�{�����I�C�]�A�����1r�b���9v��v�bbI���x9�	E����WF#���������	��r�6����D�V<����Cuv�!�v��]���-�bb���;m�b���v�	�s
��eX��;{�^M�b'�Cq�L��u�zh�N�yWC
$9���~�g������hb�i�z�hb�hb��1
�������9���Uy�=�����w�+:�>����v��g7+�~�����������Qe+�nm�3S�_~��f��W�QIQ�6�!���bWi�k��U�Cvv� ��1~~��������~���#��Bm^����?�������Tn#����3:'q/������]��v���:����b���bv�j��f���~�������ua^#C?�f������r0�6�X]S�4NV���5��9���#��`p��v�����,8jiu�/kw{������nF@�w����%V���������E��R�|���{N�Z#Z�W�v�LY��0xX���j5(O��xw;j��
+B��=���I�6��J~y�S(A���	�{_"��.#aWt�1���/��nO5b2.�D$��g�V����oGh�^���xlUT�z=�
|".R�^���q(sm���$�5zNa�$(!^���4�mc�TJz�=�Q`�)�fEF�D���c�Qv����@Y�.���gW^2\��@u�����yf�7�v�����a#��B�������}�y��p�#n�����B�;w~��F�nDeJ%��� ��'�z@�����*
�	�F=5�-<V�\��P\�[\�Z���P����5B����7S����I\���oZ2&���\�L�9�U��'�KU�^H��~�2�42�$5���	�CWlLCm�� ��1]������I�[���v�`�D�x`��>���v�_n]���wim�<z�v�{�7�zORS���&7
�7�����
~x����*�C���Cvv��\��k�b��1���<,�f{�<	��������_~��g{b���3^5�K;|�������O�p$�p(�h������.�0{�V�!U�LA��1��yv1
v�A�m1
���?����������v����[\��z
�2��x��AON[[9g'@�G���iu_�v��V�@�:�i�{���;g`1r��Wl��lLAIU*�������~�u�Tg���JY�MO:�j}2��uk6�y�'���W�yv1����`�;;���.�b;i�kv�b����g���cE:+~?gm����ZMB��b�>cx��i�9�%l���]�����5���3]�uv�����m���e�LC���!�����U������^��svw�[]������������������
:��Q���w>����������+������!�v��;LC�X�����H�yPH~���~�������]����
��:�1��.f"�#����v/�C��>���~������$:������V$:�� ���`#����	���+��p�W����\^{%[����������+1����{o�7Y�A���j�B���z��`��PH{��=U��'����wT���x���f��b4�}��?��85��;$nu.D���������B�`��V	��$;� �PH[T��;�R$�@#���Lt�h���NE�@����+���g�%��Fri���5���
M��%gs@�c.��%����R���zD��um
V��j�n�i��z @��"2No9w;�DEUa�L�u�I�1�Pa���������+����3���{�y�aPQY�����QaU���l��H�����1�o{����XEDQ�8V>~���~��:�m@(�/������-�4&cyJ�&�������Z�y�t����c���|�E��9�z��ijG��{}�a�n'��=���I&����.)QaaXC*��������0�(*�{��O=4,0����~��� �"��ns��7��"�0�(�"����kM�XV����s�w���0�v�F:��������[D�*��
��*���8p
�TI����	y3��:��w�W�H\}��t��6�o�����YN��>Q���T
���Z�;>#��=�+�nx�8DXK��M\z1���k�d�=��~�o� c_P���3s���TXU{���}�a�aX>�y�Y��""�'9��k���[�UEUU�&�s[��ATV�>^w�u����UG��fB"�����"�kf����/g��B�������m���rV�[��m��5�x�=�����J�;�V��z�����VL+��$�y���9k�n�S-��8&'������^��P:i���'�u�7�}H%|��Ko����F����SQ�aX6��S7R��
������s�z��������g�uw��Z�EaE�e���k"�8����uQE�Uh�}?%u�srz��	
+}{��DT:�I�,��M����OMn]��qqN�s]ys�����M��n�6\��]�%g%��@����f�����z��p�pv�K�1����nT����k��;/\�u;:���~-����VXaQ�����:�r�`V]9y�����t�*��*'��r��W���
����AQ�!F��}7gw�VX^������TTDE��>���uXT|(��k�t��-����}��8@����G;P���k-�
q�X�Wa�0�u�&T>���R|��X����(J��+�t�w6��_&������9�(r��J���:�\nl���]��w�����s{F"���	�����QP�"i��e�����+/'�o'��}�DDEE�OF�7�3L*����M��u'!��+�oo�����*���}v�y�GV��<�z�N�|Z���JsGg�yy���k�
���W��y�dx���k��
�;�wd�R���N�^��������{%Z|C!W���wH��2�7�c�.A���&���Wn��<Wn���Wn� �0��z�f�}�"���#Q�t���Q!Q�PS6I�����*��0��eOm�� Acvr�s�1PRUO��_O��&
����*����6��U�+�}�f�����������ohJ}�D��&�]E���j�W`���Q�����"~<F�{'���*�r�����6��z������H���S�8�s�h�qQ^�����fx����aQN�����`��l�2d"�+�����k
*,

��]�����t�F
���)�=��wf��"TQaUO�k��q�����Yu�=�Y*��������������\x�~����7�es�v��\�9�1)��G*��-��k�qU��i���v)��������� KC]���},NlT���6����vxM���O�t_[$�o�r��{7�������TUQV7o>��9���XHD�9|8:Wy��X�E!D�����]���
����u��a}����������0�(������ �(�����w����S�������qf��O�;�Y�-s�*��:Y���wT�d(��n
k{
���7��Rc���k�|"�7���=L�M�%����^�[����Y��q��;��9������~�6<y��o�����p��`��+
���+����Q�E���������Ua�k����uaW<����j��u)��@E?XZ��c>`}@|>�5f�����F�aU
���������9�����.��/WMt-���n�rSG�e�t�hX��m��LP=T�����<]t��5���8�s%��?
��Vj������P���Ua��2
�ns>�L`X�=�k�t������9���*�j,"���/��w����>UHC~�u��%f���XQE�7��>�k	�:��0�x��y�^���H���U�������$"#����9n�yt��"��J�bn��\�r����8}b��=����F�uA(�������b����l�*�������x+�/S����yjY��	y�K�[6m�@|����)����m�h���O�[�b����0�)���}����!QF��L�O�o��b�aUa���T>6]����rXE���Da�XXE�'�����+,�XT���g��`UTa��M�p���v�&W�6	�i��3���z��fW�?�;��"��h���
[����	i�'n�G���a:�xK�y�qqF�i�e���CFG����p���E���s�_y��+�������,j0 �
=�����e�����0�,�O��S���Qa6������-a`D���s!� ��#�{������F�R�E(�
V���>�����x:w��L�p�����QTU�E�A��{f�eUl��:{-�+7�^��o��Uf���������<��:X��h�Z�N�y>����P��B�%���+p�����W4��[��}3�=�$�,(*�"#
�Fl�x�M�KaUx\;��]�<
L*"�+�"������-o9�7=���w�<�eo'���:�0�����""�����&j�zs��������e=���w�)��Ta�Ea�E�L�6�=��VvZ��-p��Z��c �
�
(����2����8���P��}��%4��T�Ta�aQUaU�J���_z���;b[M���T����"�6;+��n��O��e�}�����ox0�0x���/5�5��(3�[5,v�U4��4��z���v��E�,=�iI^�6�|m%{�o�#
���������AQ�����{����"��w4S*��|w�mW��0��
&v~�����*0��w7������a9���TTHbRi���U130� ���0�
��<���_kN��=���W�5TAaE`�s��S�vX���(S��m%w"��U!�QDaK���n�v��������Y���t��J��
���C�
���n�w�k���'�������M*(�#(0(��Ov�4��g�W3���������3�
(,
���+;������f��f|n�����T���iB
0�(���%k����6/�Y�S��k4��R��"*0�"���fr{��*�;�ev��z���^T��
D�QW������i)};-D#
$���>�E�EUQTT�q�]�ZsI��VH�E�Q���jn<u]�}�����~�mI6o�M�4��M�]:z��L��hG;L�����MF����rl�b��`�Z���O���;T�������ow�ww�0���y�����!XRW9���V�u�E�aDU���^e^Vb���*�>�����@D�<������REfd����jpXXTAX�~������!TA��>��n��5o�����������������0���,Uwu�XXhh�������Ynq�yJ\#���
5��K����{7�����w.�r��a�EXF(P����j��cvN5`��p�0t��������7��*����g�\��.l���/2�[�� (C�(},0��V�����L��-nV�e�����3����8�	������-��'�f� �*^�]�O�
`QFT�G�S�����e��Y����o{�o_i�*��"B�����7�omK}�����e�-SX�aDTQA������g���M.;��f������^W�^V��0����������D8��r�����^�\z��4J���I����j��������t���P����g(
�B����u��f���vf
EE.��k\�0D�Wy�i�}��D��B�{����O/f5a`U+�}�FU���[�l�(��,">������N��abX�N��!o�=��f��.�EU�PI��C�{�n�3�rFK2U��J�3��+��V+
���(���|!4w|�k���W�����P����DHQQAEQA���\v��c��P�p�.� �$���EHPQ7c7�����]y�5���g��6�����9Sa"�����*,*(��Na���Iz<������*����6��[��>�`U=�����>�59���v�}y>����PQaDQQ��v�|r;�t�oP@aUAT`W=6W9�{.���+n���I�^i������>,QQ��y��e�;��{~�=}��c&O+o��aHQUQ�TG��2�m�����P�C��e[<E��S\���O���"{�b����5���]����-���<��q�z���R�JO�/U���=)k�m���[xL�TU�>������s�	
�"��.�{�s��Da�Y����mF�6lB�,ed����b�`Ean����Wt�����J"��
�03].����UAoro�?� ���s��������*����A�A�Q�U
+�|+w�K���������.l��a�FF��gw��}����0�u���H��L�tU
��}AHU��++2w���w]d��v����'r��5uC�s�P��
("��1����/���x7Gx����y&�,*�"��*��.�y�y�����km{}�������Jy}�p��"�(���9�>����a�oY���7�\I����UU�DB��L���Sfg8�N_S�$"���*��9R5w��N^K�����^����y�������������[mB����=�����3gm�$3F��8�,(#
�
�*�v��r��opR��� Z�54H�������9��G6�������Q+H��g�k���o����������0��<�q�
x��:/M����Lj7���V^�/��<���_���*%=�}�����@QaPl�_j�$QDEb��_�=���g��,*��*��f�����5�bVi����!aX�s��b��!`AXK'��>�U�E^ve�[i������B,*����(�z�S|���!^��\�m`�b���UQXp�����%�o|�3n{�������fWofyt8��*��,(���7.��=����{�s���rvk���("B�,"*��=
=<�4��9�[�jK�Z~|��QUPKy��r�=>��:��f�����{��X�Q�aU�w�9���b"��v��@����\i}f�
QUaU�Q.w��M��s{��e�\�����L�rf�o��,*����wwu]l
Z.�����'-0LKf��(��	
(������6<�#zV�B�=�I��PE��:���3^}����}x_|�*�S%x�<���>�6�>�8"X���y��;��0����x�S���QW:&7%���uru�ao�)�.�C�����>]����}h���+��(����������n~�au��3���aXP���6���H�(�0�s}���� �(�*�������!F}��N���"
"(�2����s ��"(����fe!DET}�����]w�w���`��N�NC�;�LZ�#
�(���	4��"���h��i������`��fP�T�V�E���U��u�}=��6f����9�1�s��zEXQU�&w������S�%fm��{�����(��#�[������X.�����N��w,b��@C��*��0W+-N���2y=����_*i��������""(��{������lw����7�a1[���=jy��TU`DTX�	�K��!4mK�}V�3���Ho�{��EPAUVT���u��������qGwJ�@|�U(M��B�J�.'6N��n��`�]h~���F�[M�������yyc�[���ui�Z7�I����#�bf ����q��M�q&m���o/���Y[����}��DFAA�'�=��/���((�",(�w��yXqUa`S9��^n�����
��g�g{-r�M�Me�n�h������v|��N �
��7���������VUQ���~�kf{&d��% �"*,(���}>�Uz������5aaEQEX�z�y���gY���v������gL8�
0��,�;���z}m����{^w�G2�A�FDQDX��K>on���s��������8��������*B�#���uX��bD��a�:i)2������&�DHTT`A�G=[��W��[����(�*
�(>���(����`h���7�}Na5�E�!Q�Q9����rN����rRy����w�vw��aPE�U�
���;�q�1fQ&��#���s�Q��DTX�HAZ������-h
u�ph�������8)��N�[�P�p)NDG������\��6��y
�0�_k>I��0��
l�1�rx-��4��n�����f>��j�����w������T
_��|���J"����;�y����Qa��HK���o�Lba�aDDA�D�����"��
����'XTEaPQ�UQ�Tu6�Z�(�*�10��"v{��'
�TQQ��aQj��x,*�����*�����7}��_�PV57�+_���qscM
��5����k��y����.��xNf��X+��T���#��\�I�H��:_g(��R,�L���M�KMn���\2�	\-�0��z6�������r�>r����@�������xr�/9�g#�����lQ��:Vq�����.��2�����N�S�Q���������3�O���]+���������s����z26����n���
�^�h�c(����U�2,��631��x2��J��i��8w�R<�$���R���tI����b���I#��m��5��:�����-^�����h<���b��l4��kw�#
"�3n��<7�m��v*dmjNQRO���L�>���;���Y�V)Z������c��a������������pk��\�"l�g���������*����F>���v$+M�2�Vy�`p�)��I�
���l�<h�g9W�1�/Gn��^�m�2�2���yN������'����5����������z��V'e�mh����z���Q���P�����&�������gHEsD����m�>��w�7����@�p����P��p��!�u�7}a�R9�2E�t��!�E���m��&M�09}��O�9�W�w��n�f�x`�r��2�����$\nA�V�w�&-k�t�#Q`q�����&�S�[�����4V�4\��r���,$��!-�G��`�a�"�nX�p�67&f���Ku��N/1�	a�m�_��MN��L9w��wO/�/���yvxu����~f��)W��o���KyIM##��������vJ|�������\|�������;������q���}��p�)���.d=��^5y7/��s5���)<����I[��5���S�������jDCeK���T�V	d
�Q7mw��Q"�$���!34��6�l>�����J���8��i%{�}��)K�|^Mo`�NP[w��b	&�Bt�$��
���<�u�XVK���o�U �4oY�Nf.���3�oD����rKu���������B�;�nR5v��Y��rv���,wq�6��.�W��it�Vl����-{� ;���cK�G�8���=�k��T3�w��
�����}��������X��i���
�Z��p���p��j:���A�7Q��[2�	�v��ofq[�j���$���W+���{�P�������PU�Tm�j��+I�{� K	�]N;�7����7�A����P�{�Z�Q����+KP�ze;����,������%���^N��75,IE"vG�\��z�L5���C}���[�,X���jg<.=hw�^����`�U�����iZ$��jz�8�Y���f���H��y�u��C4��s.�{Z<�W���s}��+��N�9����&�J��I���t�H;aj�kk)f���D�]�����'���7������q�{�Z����X|_[��y��l��Uu��O*�r���^Xzja���^f�����W�l^8��nA��d�.�"�B1���2�/�Q�2��BE�K��u��J� d3�j��/�C���]��Uk&5��9��L�T�a�j�]wF�N�[1!��A9�<`w`T6.X�=���\:[�<xt�02"��4����v�9���H����o�!G��e:�v�hp���a��'�;ur�+!��f��J�_�)���&����*+K��z��J(�-�2�fWk��#��eUc�Ad�>V�U���#
����i]���Rv_D���dyy�(�����I���b2�3,�Ylf���q�_KK��,W
Ym�5c,;���U��
1ku�5��y�������k����gt�z�M������j�������<w��P]�Sq�)��3cvU�A�����L�� ���a��RN
:��+�D�6q`������O)����0��	9x�P���sz��
<��]��Yl6�v�b:
��g:�NpSq��9;w���&n�VJzS�umi���[Y}� npiWP�����:��.���\�d��u��	�Gk�N}D�Td<D�Q����v��QBt�X��X����Tp�7z��L����������&I[�c���x�������y����kE;H�O17����y+�E�t_�����>�:?'DL;�p}���=���S���F-���.k`5jv�[�k�:�S�������F-{�A�S�F1��
e�U�����t���S��.L������|DA\Z��8��=�D�%o�����{��r��Nl�;�W �&�M}y�t�1����|>�R�����&�s��Ve)
lIYk�N�a1YFO���1��U�2v�S>�������)g�a&KR��A!OM�X�)�;4�����\�o]t����5*S��R���=�E��F�f����AN�-IJ`�
�7�� Vb���9�
��8�ng� ����
F�h�-��sn���P�v�8�t^�1��A�g|�kLkE������Z�"������f0���}#�v]���R�j�n�i���O
(�k3���	6gwt��f�����,�%���6}��z��y���;|�����;�����9���`��X$=U�U"AU�����V	
�?m���|�~�����>S����;���B��n�����~*|f�v|����������$=����R��!�����Q$x���d��Y����w������tw���?\��	OLm�v���|.�������C��$9V	�� �Y�	j�H{�D���������]�K�~	��������=p�E���k��,�kVC�4���($=�D���!�U"B��!mPj����H5T�}~���P�r���O�����gw,M�V��a�JB&��!y�@�xM� �h��!�X	j�AU���@H*�$-�	�/�������~o��1�;-�V��r��s��3g�wy;�w��m��o������]��C��%�H*�U���!�+�"C��	z�H?��������{����e� g]�-���m�����vd=���;�ah��Kl�����'�xU�G�=���j�HZ�H[V	T��`�����;���������x��i�)px���_������7����a^%����;`~i/��<�H=T�����$=���5bC�X��Q!����������[u�{�����IdojG������������}�������ZT��I�}����U!�T��`����B�@HuVC�TH^V	��C�N��[���^�Dw���2)�%=�p�@���������^�x7��|.� $=�H��V	U��T���X$9V	
�������p�	����uu�4���j>��Rs��T�@1��(o�������~WL&O7���c�����]l�^8
�%�����K�g�1$)TL`;s���i=^��%�����WL��a�y���F��(�D����T�������3~����3u�
s���@��>�%v���	�g6���" ������j�9�[�����8s�t��OTepC�yTc}3�\�'��*�_���K kM��^����2\�I�3�7�<���/�Zb�g�� �:q���b��z{�^-�F��<����r����33~zJ-	���B�����!N$�/'6����K,mK'kF�~�-�������a��|u�ae2Vt`y5�\ 0
�}���u��:�QU���^�mH5�����0�!�s�[0�O]��+�'�P���q����i��Q�=���J}�V�*��W��8�a�/"$�$��;��dwV;��L	�����Q�
�c"��cN���VI����`����!�*$V$:�T���H-Y�X$?��?���������2���E=/��Q�U&�^�>y��f?��_T�J�C���Z��������!�THV�*	�$.����~����|�T`Ga6��uv�s�������v��=��7���a_���{��M���@H{�A!�U��D��@Hu�	��B�����������{
����InLj���8�e~���I����m���B�����m�����+"A�H������!mX$:���j��mX$?~��^����_����FA�SZ�<��r.�b%�L�x��W^4�A��W���k�uX���
����!�TH{U"C�PHw*C���������(��I���7���y��~�0����:s�:���)����r�f����|��!Z�C�V	mX����`$���!�U��!�Lho���D�Y�t���)����}�1n�e��tf��gz"#pr��_���5`���"C����X�UA!�U�*��j���V>P�O�?�y�v_i��?.�7�Q"������v6��0|�^=�::�\��~����a�C�`������C�VD��D�U`�j�A�bA�'�+�����/�������[	���k��r>��(T�,�xp\^������O�~���?�w+��������Z�����B�bC�$<$�|�t�0���a��kE����0@��Y����Ut��������������j�C�TH*�j� �`$�$;������'�#��#gC�����u"lS���P��Y}���U)�(�O��<(/>tr��c���y���7:�WQj<n�����SE����!������g,*��=`����QRc�q����w��{d���eP���V�"u�F��*�6���z)������<6��:
������xVF��]�,�����=�W��&:�n������g�J���������f��/�F��\[x&�g�B�0�<_b{n�'��Mf�9��6>;Z�����g�'�d��k�s��|	���}�L��#�U5�Y;
�P��)Y�eWL=�[l���:��1.8�����E%��{z�y�/>��v=u�*�}��i��U��e-J�9Y�Yz6�8��F�=�'���|�`w�\Q�pN�Pd@'���5�����<��^p��X.h��g������wsX��_pkl^��Xs���Z(.�je�z��TB3�^�*��C��s�,[�a��o]�3�&`����Cn
r�W���r����UH����yT���!mPH^T����~$s���?0���u-�6mc��^�$1����K��GH�rf�����TH^��	U"B�A!�THwU��X	
����c6q����c1�����r����Js��o:�s�4�]�~��'%RC�X$�$-�!m�$=�`����UH�mi�~�����s�~�����5���vT����j���c��U s�n6��������$�<T��C��*$9T�U�5A �H����������~�+�XFE�C}�6������v}�v��v���<w�TN	����_V�'�5�{U�����V$=������j�!yX�j�$;��}�������^�V�b�F7���-269�W���~�@n���V������^r��7d��������*���V$=j� ��HUT�U�C�������S�k�����UL�30�V�o������OQf��H��FE�37�����}[�A!��	Z��Z�$=��Hz��C�Q!yR	�H���h~�~�����L���f����s�#�����mh���XQ���DM��W��Hz� ��!yP�bA��!��
T���$>�1��`��q��j8��-OF�(��E\�uyl��[���u8�s�������]7�~�H�Q!UbC�������!�X$=V�����j���}Q����z�iI~�����@.N�c�-.�v�M�~���4{powG�n��VD�Z����H5X���V	���dH[W�_VT�%�[��'�����,w�����usU��Nf��-����G'�����T���}a�#�;j��}�!V�P��%s�2��z�,���h�N�2���2va?��\����e����nMw��q��u��^�8�c���k�e���v��v"���V ��+���%��y�]�
�s���f n��<���8_�<NV��VaZxa�b4A���=���������H����������D��~�M���SU.�,����UW�R���-�g������)�^1J��{w�5o=���)�@�����k.�x`�u��[]�3��
�H�]C�q���-���y�������u�����
�/R�C\�0a]����4��Lz�r��gQ���x�F�>o:hA-�[K��iD6|�+~����y�K"xz���Sn��W��.n���D��m����n��k���A9��k�����[�P�����K�K�5�rF�X���(R������q5f%�������T
������PH*�V$=��Aj��7d�<7}�������g���Y|�;<��0^:�h�8()w~�l�����?���������?�V��yX�j�HuT�����VA�`�&�����I���-m��):cvo��7��c:�5x���_�����O���x������?�=TH-PH-X��T�
��C��!�TG����<$�@#���o�YG�;1]�������D�jp<��z
�}��~�W��}>x������C�T�D�uT�UD�Z�$��A!�T��Ud�����n�����z'�[�,��po������{el����c�m�}��������A��AU�T�j�!mX$=�A!yH�VH�v}���5q�q�>���
�V+������'��yhxhk,��_>~��
�TH{U���H5X$9V$=�H���	6H�ZQ�8�U��u|�ONv�#4N�IO����Se��6�C�����'��H���H{�D�j�C�X$V	U!��������_��d���Y[��,`�{yF�y~
�ub�t�e��&$4�Q�?}��_���j���V	U�
��!j�$TH^V	�`'����!�Z��w/��~)S0����:��o>��^��`JK����B���_|ou�|�<����������UA!U`���$=�����$=�A!�TG�6H#���G���M��l��v�c(���L^.s�}����NV��C�|w��?<�}�~�
��!����dH{j���T���C��$=���j�C������=�������%f���Ys��{��^���q�X��K�J9�Q�|(O_��i���i�V=�
){��	P�T�Fi�h�0��a���}�A�@mk��\���[#`W8�+DO(�������V�5RhC���������]�]�;N])���3�T<��\��	�s����G"4U������cF�����<��UQ;��U�"�Mh���Z��\�,{� ���Y��~/���>�g�t��:��+�M�g�s�hd���]iD\*�����X���V�yG������p�����_�^��;�F�b|8c�\��2���(H�=�2o\�s���/�}�9��U;X&����m�FV��N|kV���9umX�Ko�A1�a��
�^k����:������
q��6���!3%�F=U�&eLtU`����rQ66��RJ,�]���yQ������\�NO����M< ��*
�Smu������:�[x[+��w].�������S��/y��_L��[�H}#���X�����	j��UH*�H^THuV	UH6�HwU���_[����;rE�������S����OW�F���n�yt�G��
���>9�	j�H-PH6���PH^V	��HwUU"C��;�?z�Z������f�t�-����
�g��08�;Cw�y�����H=V	���uPH{����X����j�!�THw���5�z���U5���u>�3���ii�7�����/z�]��P��T�������"C�����!�U!��H{j� �X$PIZ���?�v�~0#�Z��n�3�����T�*>.g7(vEf
���l�|<.������5A!����T+�Z�HUQ!��$7��������<��_���1[�P��n�~������������>��m���!�U�����C��D���C�Q!�T��!��Hw*	��=���������y�=R��;)gt��|��(w��f�%^����f5O�����R���|$�C��PHV����$-�	j��z�
���!�����������F�����um������>���S���*���M���66V���F�!��	���T��"C��Hr��Z�$=�A!�����h/�0�d�����M�Q�S��%*��i��[V��{z���!����8�w+����TT�yQ!��$=��!�YTV$�i=E_����\�����:/6�	�4_��S�;����e���Av�a�C���*����$;��C�`���
�D��H{���N>�����	�v�����7FluW����KN�
E�f(A{�����k��<^|������x�vl����aP1��'��)��3A�P����u,�������2���^�}�99��v�����L���!8�]��\u+��]�bz&A��J���^`{��s{����hxxk��S0���u7"1x����0�r�6%����4%�����C^���t��Cdb�������h��t��=
��2xe���q���=YJ&�%yw>��������5�1�,�������_���[��y�B�
]1K�I{�T�;��c�F�N(��^}Z�ud���C�����J�#04A��7�%������:��9�n�D��j-o.ac'^sf�{��N��:wZ{���S1�=-g0������Z��J��n��Pz{���	kb>����_��y����tP�:����}�����������Wj{s��
����oX�d>lp��!�8����<}�������
_���V	���!��$=��C�����"A��AU�C�������G��?��?d�����k�i����BV�~�6	f���HE�P��Q��{��f�#�uY*D��"C�Q!��*	U`�mPH����|���W���9b��y�u2�<�o@?��_z�uK(���
0WfG�pr���=U
�D�j� �`�z�$9T��X��T�A��6�s��!�nb�6wQwH�;�x��2����%o._^hI>��}^���miz�Hz��AU"C������C��B�A!�O�g�	���?D�E+M�������=��g�q�n�r����R��'��GX�����U�C��H[T��!��!��$:�"A�A!�W��}J����JX��`��w�����L[�� �=\�?����w��<sq��k�{�/U"A�@Hr��
�C�PH^�THz�	�>xoi�n]��_DW���.�(�j9c��������M'����e�C�m����z���bAj�!�Q!mX��X$:�"B��Hz��l����Z��E�b���s��5��Dw�C��~���/K��@���M>i��A��Hr�$9�B�`����B�Y��C�X$P�	�]S��_�U"W�*�wI����M�t����vU��R�V��w���O�U�B��!UR��H<��Z�H[V	�����x|~��T>5�M�3��V�DE�����`��%�_�*�\�Gb-������^�Z�H6�$;��!�V$:�	�"B�A!��$	�|G���)Q?R�?M����������w����o�{.w�e��w���:7j�.VG�c��6�%>���K�d�n���y71�����^�K���v�TYN��xi�o0N2�_m�V��w�DMv�f���`�:*4���0-��SP
x���X��hR��5�U}V^��v�Wxx7��/iU&����m�O�y������sp-CU�d/{�9�2�t�.2�nV��<�
sU<F��`L=OJ�;:�
��og-�=�K:�D�1EO%�����qj*�.���Ns,��8N,��|~��r���
x�N�<���Z,�s����X�:`�@,j�=Xx������i<o�����u\]�=���6��J��b�����{�@�4Ts��+������	�����:J���F0�w���N`0��)�]q��g
�1�Nq�a%�O�\3��b�a<L�f�OsVL���cW�S��
\�w&�
����3�_����,����`4�r�}�}���VD����T��� �� ��$+T�A�v��/[����bD�����k�+#�YL:��{���-��������3�����C��HZ�$/UH[T�U�C��U����Ho���~�������^�nWK�������%�=��o���5�#����������UD�5H����yYTHUY��������$u���b���QY��L����O�`sD�D�?cC����{�}�����	z�D�j�C���j�!�X$/U�j�!�R$?�����#%��:�.��[���	1���Z���[����x������]}����HB��!��$X$:���B�X���!�Q!���������-�"d��R��+Mw pZke�����w���)l��3�co|>I0H{U�!mX��V	
��!��H{U�yYV	 ~��������o���	�y���fP��w[y7��_��n}0�$��'>�����������Hw[
B�`R����m!��
B�JC���_�?�������d�@L�����;(����;��������z���g�j� �������!mX�U`$-�	j��j�M������}>{�|�����Vb��E0�����n?a�@v�o��;������{O���!yQ!�����$:�	�"A�A!�Q!mX��?�~?������7{��bM�Y4���yj��U����X������|��}~~��� ������C��
��mR$=�bB���U�@#��'���q���n}�~���ws�j�G.��G�>������1�����xa�
K9'k~b2=��E�6������+?;1�����z+�5(�%v�����&�M����]����t�\���q`��bq-��6�:g�Q�q������cA�b�t�fy����S��(r���{��5�7�=�5-O�2��{}�lxee���wycb#�~{V���<��.h����|�>������x
��{�*rJrz-����`x���Sl��s0/x�$�J�9�E9&k��-
N]����M������6�����.9���B'�]X|���m y��������J�zr��'6������E}:.��[*�y��l��V{q�t�U�Y3;[�����wu��Ej���4����!+r���s���*�x�qc@���R��4�����h����\9�|������� �.��s����K��4�����7d��jj�9j��;P2P�����#^�n�����������*�HZ�Hz�"C���U!mR$:�x�G��8�Em����E���mu�.�������W�\�S��S5���������������B������� ������C��C�V$/U��>^�?������o���������_��v�j�(}2{5��x�����~����=������`���$-T�
�� ��U`$U �I|�e�@!�1�%�s����4�o*h�H�i������-����f�a`�2�Y���=�V�m�����H!�9�|�uPHw+U�C�V$=�����$���������i�g������y�K_�=��z����}��9~�=�y�����T���B�A �������`���	�H�Z�Hm�����������K��G����eA�'p���f�|��������=�������U`��X$�Hr��"C����<$�Al��m��v��[�>�������15O�
�%D�.�Y��Ml���}�oS��������C����!��Hu�	UHu��A!�Q!�����������|��?
�b���I��\��2-kKu��&��u7��vo������~��*����"C��H^�A!���"AU!yQ!j�$.����?w�������Y�$Y��h'J��X������{P[���n�k���T�yX���$��R�X$/U�B��!U`���n���mb�$�cao3� ���E�m�c}�������S�*�&�����$D:�	��A��$9V$:�"C�X$X��X$/�����~��"�Q?�
�d���D����r�����m\Ak�2������A,�dL���tf*&;}�1�.�@��0��C���4��7 ekJ}K-z�V�bTP�����ge���g�n���!=��u6A���Y������K>c��y�
]���V����=�4�j�w���`���'�U����������~�����
�����wWjc�7}4��y������V��{�UW����)�Y-�{\����c\
�;*�&��<f:�4v�:8K[���[����a�4��]���#2|�V�<	�.��`r/�k���I���
���R!N�&�[�p`����J�KF���L}o��f����G�#�GR��Z\�����gr�W������T`�;NA]�GO3,�4T%`z��7=��
��w�>�m#��\��j�&>����$zb�9�0���M��������z[�v���Z��<�(�����+���/��=X�]ho�pe����<���w��A�D���!��$;��C������$��Hx|�}Q����m��j�'8�/$����6Kb���\�j�dI�S~M��������@�!�T
�	�D��bB�H�Z�HUT����D��}��������>}�����*�������.����C��:���<�����!�*B�A!yY�D�j����A��A�@���$
�1�6_e��F��w�����J��ya����V�&����s����������������5D�j�!�U���B�bAD�<G��'�x|3��S��������[��)�����~�@%�GI4���������o��~����"A�H��PHZ�$/U�j��5bC���I>x]>���}L����5.�h��5W?`]z�&�u�>��_-�����}��?�sT
�$�$*�H-X$=��!�Q �|�6Ixj��O���w�^�V��S�u�����'��+�*��R�|�|����n���^���
�����	�`�UA!mX���H^���$�<G����(_w��*����.]k����t��u��pg;
�N��VFs�k�����?����j�H*�H^�$:�	V	�@H$�c����<�w7�X]�7�f���O;���q���Kj�P��!&�a�/���������?���������Q!�T���Aj��j�j�B�D����`����o��O������������y6N�9�n��ZNy2\���tA���hJ9���������K� ���	��Q!�VD���B�D������8�}����O��+���
�����mx���_����L4�U4J���b �z[(��b�U���]�k9x>�������r�zi��v �e���V����>�?�A�a�o\�Qt_M����@Aa�����>������r��x�x��)��;W��9����[S���8c@�d+�O�|>
��0'v�����4F���j�[�(="��M4�/��0�$����=MoW���N.�%�������m��7s��e����$�����q\�P6�u��pUF�@��z��a������<���B!���Z���I��6�����h��!c��
�vf��s�^�K���:T�b���@_�:q���I,MA�Sk/�5�/{c�/��Q=-V�l��)��@#�Az������M�4S-�o�\����oZ�x��d��B�v�M����XsgfN}`�gc�N��W!����=*h;��fX�}��]���6�
��s�}I9����B��wTH^T�Z�!��$U!z�C�����?�������8l����H�����hu�E	��N�7�;SU��ws24?��H!��R$;��!��H<�V	�U�B�Y���>}������CL8��X�������*s�����������w}����|W��!�Aj��j�$TH-Q!��$V	mPH5Q!��������������K��H�Sfu,��5)��n����c
�>�n�>-�e�s�>���
�A ����H������A!�-!��X��Y��!��������b����B�uiq^nY��R0Jf��W���,�wo���K�>T�`�����UA!mQ!�U�!�T��~�����y�����r��t�KR;��n�'���YU���E����kFXt��|b_"��$/U�!j��!�U�B�R$-T����3����4��~xr>�=����P%����t��9��$)�����^�������}^��$=j� ��!�PHZ�H^V$;��C��HwU�
��~��E�Ow��y�������\��][���CI�Bb����k/�>I !z�H{�@HV���dH^T�"C�X$-���o�~��i3������S;�f�l]��#]i�}*}��Z�x-tW��kN�����>�D�*���T�X$+T�TH{��C�V	U��|�yo��G���w$�i(�+#
�������4^;�a�
�z��q��|
���j�B���bC�ZB�T�yX$/U����@?�}|������J�����f��>;�P�68���/�y��������=���R3_��W�����s���:���C����?U�������s��"����Wg��0��D����"����s��/�F
+	�������FT���J����"��
W=����b�Da�VV��/���U�����S�P0�T
L5Ui1+�:n��Xv0Kpm0���1Z�]3ff����r�b9�E
]�"�l�������7j� Y�n�f�"*gY���y�\�P�[\�__���!QUTOO=>���=<QAXDX_nY����������~7�}�n;���0�>���;x�o!�TGv{~�|{luXFTT�'e�KL"*(����+s�f*B�*���Io���'0�$�
,*�B*#�8h90?8��-�9�]����/W��$}Wa�~��Vj��/t�����,
�{�����#4��i#N���L��i�Ng�Y5<og�����������`D�f�\��\L(��*uk^�*������~���}q*(
�w�K3�Onm����(�RNr��w��"��*�5�n��m���d ��
�2��P���Z�����K��Bc���Nw������
�����a`w�)�+�z��� ����{C����-�W�n����A|�� �������G�/�MX�%N������9������o	������-�w���������=��X{�^vx��}�AEPQ!v���e�*��C��+�<���!aE^����:+�(�^f����aAAO��g�u�LJ��������]�U��y4�>�*�%X�g�d�U�������� Y�hl�Y���]�r�����{]�g���/�n���yt���GA���������n�,��o�����������%XQ�D����������((�C"�&���ey�^��!�VaG�����N�0�#
#�}��&����s��EEA����0���O�Ic�����E����Z
��,
��/=��P�4�|��)�2�7�r��f���v�B���O�j��YPU�lN�2/�{A����D����-^��kD�A���c�!����a����{.kZ��6%��"r��j��z���__7��<"�(�,*I�������2��*�/n�tU��t0a�Q�������v�y��UPL�������QVUU��{y�v���aEQ��&x�}�e�A�}���p�N[�T���Y���IR���I����2'M��)�YD�fw=@����
�<������������k��2@������U�L'��w�{a�y��p(P
(��;����+��������aOMy{���"��o/�k[���x�BB	�I������H�(��K���W���%UPk������|��PQ
I������y�(��WSQ�cP���
�d��i�}���X���g�e.���`<|�)�"o��|���3�GM$�)������H��z�������ezkb�"Dn�����ua`XUC�}X��y���"�0�9�����2X�0���������.�LADP�yu��o3���	�W���^�q�c�o���T`F`a�O���<���HXQz%rVh}�w����rv��%=[����H6�n�}���Y�������i��S�U;�l�s�y��v;U�����+�C�(�Vv�-��<����$������=��RP�Rdv;>=�>3���"S�7����
��y����UAF=���-ut�U�Y������/��QQ3���}V�TaAF!_l�s�W���b0�#~��w5�,`U@B�;������x�=�knf��S
��f��W@�������#��>������<��I%�q�����LU��FE����9��s��zc���2ff$2�X���
A�%R���yG�~�g{��aPE�i�7�����F)
�^�O�;�tAQEQU;���QDV�����nrA�Q���5�p�u�B*������(��H����?=�{��FEUPe%��.��������|�@�}����k��H�;���%��9����_p��:���Y�?	��K	FVU���<.������'J%'S�kU9i�+J��0$�TC}�@���1'� D0�"������-.Xf��l�o\QT`V�suXQByG�^�N�vU������Q7��Kk^��R��{\'d�0�**�����P�0!�#ER�a�q�g��Jl4�)` ��DJu��%;�x��F�z����B�"�L{s�v@o��%R���w5$��]B)���19m�{j���n���y���zx���|��Ty<��w�,"	eO������EaU�W{;���o�X�����z���iT�QQ'��^����:R�,�;����D��r������!Ux��*��v��Ua��U1���k�Yl���!�n
��L�����Z=�sXB�{��\��yIAl�-����<�\�O%2���`y]�������I4"q��,���a'����m�y
�'��}TU"��������EQ�k��_w���6�TR������x�����*���/�������yEETFFf{Y��%QU=%����q��
�,}g�����o�X�9����'���&�+���\hT��{vQ�v�����|�
F�a�}|�������N��k{���'s������]A"�,*�����v�)����k���asEy�ZZ�����,�����
���*-�1��KT�Z����Evn71]� �(����z��r���9�w����y~�st����Jv������U�����'{�^�J�����y�=;����.�Q�a�A�UU}>���*�u��K�'�����7���yAaTAaUMw���i�J��.8C���h�����P�AUQ��5Y������es
����w���%}�� EE�b�y';Y�:g�s�d�q���'9|&�
3r����]���D�[���)�����mo���`)���"���^���s+
�c��3�;��l���6���8���(��MQB�
���{���QTQ�!'j/]hU��Y��������XaAR5S����p��aG>���q���W��(����>�c���0(��h��8����O�6#Y8���++
�,#����p�����s3���0
�����ml��Vz��w���]�������,"���"����Z�IL����[��l����=��
��������<��w������	�Q���WW�L��QaE�a������v��������g�s���w�/�<��%TXDU�U�a�D\���s��n���'�V�d��.������V`UaAq�.f{�����a�{�\o��^�r���F������0��
�(`��3tNxC4��^T;.�n�2�XXQV�F���7WV��81V[6�sB����%�
���
���*���+��^���L���BJ��P!Vf�w��w2���e��)*��9/�+S5�E���HLe�,���]_�����q?+�*�y�B�w��i;�������(
a`s���c�E�������K� �GW�_L��{��=�rw�����{ ��������rw}qAaAXa���f{��y��*/�
�}�����0�����_N}[���0��*(��
>|(|��"���7����v%�;�T�M�{���Q�����w�z���1���v%�p�O��U
A�AaQ�����*����e�v�������0�(����0�5��j3��!%�����b��r�;f�
��QKL��rk;���{��=G,�������XQQQU�!��!�X����P��%����
�#
��	��^��\��{�^�=��.�u��@�, *��(��, i��X�V�*u�]���f���EaEAHDaXVt�9W�J{�����G5\	�
����(|UUHVQ"{����K|�K7��Z#sjng��7�������v�6�����
������A�mb��nr~������)>K�<��&W�[��Y>�G�js|�+C]@P�B�������d����.s�s��>�w��B��
00�{_m�:��w���(�1>������EE\�����4Q�`PT��������aXQ�P�}�(EDQ^+�7�W7=_f�"����
 �*�#��_N�������=����{���L���1TFAUaa�~���4��VY`4k+t���������_�]�U�PUa��U���EJ�6���qf�oiU�������/���XUU!QUUHV�����qb�RcmVDU�"w#���HDS��5�������x����+�X��MW=�����
�
"�����1��,���sp��l�����O=�b�������XAQA�����_s�x5E\�n��Or��{����� ���)V[=�vg;�s�'-�fmQ6n������W��uu��2�l����N�2��gN��{���gaQPUQXU��a��X�=�;��8sVs�Y"�������~���O����t�/+=���}�,{�k9�/�������d������t��?=zX�Jjz�Z�71���K�B�([K��l��~2�����X��f����I�
�r��}[y��L1�EU�
�����b_����PFM��Q�,�D�H��.}�J��(���mc&i�8���"�Iy��6�i_L���
,"*������aDb/k���y�0p���]�eP�B�QXEaUTF-��W��_s����;�����,����6�*��� �+������<�3���5����z{����Vw[��:*(��C*�����gG���p�ru�;�/\9��_99���M��aHHQ)���t���=��Ns2����1�!XaA���mmd����{�<�Ms�����_;����
B�"�����$/*���f���$�.�u-�{�V�X`��������s���2������
ys
�������zs�f�|��;Us�k�S����l*�0�4^;��l�ra���L��:���n�/����	������}j^!N��U�{��:o���_��W�P3�7b�x:����-��w8g �F~�=�������"=�=��h��* �����,K~�UQ��{��DEUDby�~{_<���QQ����g���""����_���p��"�(����1QDAQ��~��}�����]�����TAQQaETA�)������r|���f������=���,(� �� ��*�,
�(��s	�{������y\���vn�{'}�T�EDPQ�EB���<����m=�q����y�z�2kbaEPa�����2��L\�v���B�]HoN�g�f��;��T`T`TEn3�T�\n���Y<t�E��,��TPV@A]n�s���h��#��^��5�n��oN]�(�TAaQQ��������&K���8c}����O+�p(���,<��s�&���^���u�4y;Q����0�*�(�
)
����=�����������%FQT��k�F��rE�)h�h�N�0���i��HN�pJ�\�[�����|o���b/]W5��(J�YuM�w���~X=}Y����[*�������:��gK�a���[D�]����^����
�*1����7�QH(�"����g��Z(���I�_*K�nw�(��
��o9����e�����(���m�y�|�Q��w]������,,(��E}����v`c�M�����I�E!DTPaDPP9���}�9y}�|{������1���jk����
("�C
f����^��<p���Ws���:(�������}�U��s;1o,�����'���=fI��*�,,B���(�l�����V�=������g�F���V�T�`�����(+���=x_+�o�'��p�+��Wk/��{�s�TR�"��0(��nY\76�����9������l�@|�*�������x�����j�i�V;�O2�q������B�"�����5|�SE���$�����_T�:����L�\�Y�n�7�UT�E��B��Y1;�:w�u�
wb�m���98��}1�Va!�Q�Q|��d�\���8���|��2,�����9F>�\N���-��v�9A�"�����b�jw�f}>���,�����&!����o�"]����F��r�����g�\�{]�aX�a?O��PTaQ"��f���"
"��9����*��pUUaUa^���YH�Y������1&��w��$FV�s��S���EVDGh�;�M�����
*���C|���-=���-�	�U!�B*z�+�%Au���������m��\/&�7�zTUEy��>��aDO�N��b��f�]�U�#��)^zl����6]�o��w����Gq�P�=��������;����o����[�������rg4���#��"*B�"���N�����{,u��4��n����:�T�*��(,0��������r��]�����y��=����3����B�"��
/����g<�t��9�wn�{+�����aFU��{g=4���<��Z_7����+��RDV!Fyx�vr���r[7r��gi��2v��q����a��C��$""�B�r����N����.��=�+ky_�ty�E��$����YB[���.��7���u���}�P��W�Y�Z���x�ry�5�����^OZd�����i��E���
qs���
�**�0�"���z���F!�3���w���J+*�
�"�����.���(�$0�"�������������QE>�����%FbDTDW�KT���$0*"
����_�x��ATEEUTP��2��s���0��m��D��c�g}�o4��bi�zC��*$�-S�c�K���k������~��+����N�'&+��h� 
�	 �*	������b���)x���~fJ��W3� ���gzkA���Xw;�'����J[,�8*t�|lAeoq�W���4���we��j��s~|�6&`�{W����|8#\��Z�m�B�
���K����p
V2c�*.��F �Z���a��|������Y*5�������#ww��&o�]�h��G��E������s������f���MFf*	G��eq��6����k��K�cUrC	����[]Z����&��54���
���b��>��o&=_c�2��a�	�����F���,����!�pE���	/n��n�{�������oa�S�h7}j�jJliT��(&+�Wv�+�H�q�xYF�!�/C�
7X6)��Ax�	.��3'\�;�Z���k����w'Ey+����;�����`U�_,�F/+;lZ���"�D��u�\��g6�����c�������0E����t"��TS�%Oq��*����Z�7h9��3E���{��|�,Kr6lIu�2������������t������;`sf$mj�yF����@�	�R�co��LW���}����P���Y-���FO��m����mH~Fn�h�������j������U�)��*��*RAG��U�B<#;��4��L��gweZ�u�F�+iP�.:m�B<S��F#J�b|�*�(N9�O9�^�X��3��'�AZ��*�l�����z�'2�9c*`������s�v�
U�s�/h�B�.B����AP�r�n���<
g���t��l�_>�>�����(2�:W�S�<�5@"A����;_L��R���(��/�@+^",�����U�3��"y��B�1����*���J�Z5l�Kb���[ �
���j����:h�������!meZ����\�o�~��<��%S��WG-��dg)��P<��[���!�N�l�;q�]��{���f:�y���tT]3rN89p�hq�^"�����Y�-�lw�41<���Iq����f��py���xL�/����n��`c��q�R�����:��YJ�$�����4�s���g
$���$�����bM�������|cH�
���Pm��T��4���:�4�V��,����%��>����r��I\Q8A�a�T�J�9�4pC���v�������f8�;Te� K�z����[��6���of)eM��J��"���a��R�DK�W9�����Q�v����4����b�s��[$����.�������9����3��]��3��c�,)=���)��k4���c4;�V�sn��a���
,����K�PQ�[2u5T��'���7V�3�V��k5*V��0f�&K��
l'x
���U�)�����7vX�i3������v���.X��W7$D��6a�X�)�:��i�NK;ut��n���":�=�y���IVb����U�W��{����Z�n�1|��l-Xt�,�3����
���5	hv���P���v��3K�j�����@�w*��L�$��h�H\�/�Xo��TXh��UU{F�=�����%Br{������Sp����Q�w��y���h%Y�u�au/�����umZw������.������`'N�]`���5��&Y����J�
��qV-����a������#b��_
�qj�Q�R�rC�$�<�-�J��Iz���� �i��-R���tb��������Yb��1������"8��
�6w1
�6q]D��/n��s�mI$��L��IjJ�%	N�pY�7���x�J������b�rPI�
�S�7�H!=?��-;5��l������b�iB1�e�������Y�$���z�:*�*P�.�NP���
jS�cwE������;n������I���M�w�a��a�}�s�bKj�.��	�^��}�KI�81��c�a@���Fz����S�b���p�����\}�+�N0;���*�s0H;d�����g6���f�������2�'��El��������	Qy�9�;�n3���z���\�����y��9�gb��e:���F*YI������e(�`��t�/T`���s�X}Xxka��^���y�[��{�����{{gvQ�U`{��jZ8E���c��<�0��9;!=�����Q���f%{��}�UW�dE����xg2��������{��-��v��m1h��A�DP|�#���Y�#3]a�����)�V=��P��Z�b��'���%G�����;�����;��E�^R�����asw���lIcG�m����8�GGj���|#������{&�-9l��p.1�k{"��4���]��m���+�!r�l�vZ���VZ�J��e�FV ��w=����p`�Y��� ��t����o6Eh1]�QH_DQ�a��u9�rnG�0�����\����
N47�����|>���<�w����"B�X��V$;��!j�H6�H^�"B��HsV$>����o�_������n��1�D`��M��S��xz����P�����������k��R$-�����=��B��$=�H�UbAF���#�xN�����������c,���9^��!A�VL�6��l�g�
�>{�{x���\�THZ�H{U��V$+TH[V$=�A!���I>#����Xj��Fl������P���RT+r������t��p0�������~�'�U��D��PHr�H=TH<���`$;���6O��w����q�Q�{>�2	Z��yf���g�c�c��q����/xy�|�q�}��u��z�B��!��$-�A!�U�C�X���VO�G�$<���<����4�(�s@�;�z�
�YGP���OP=�>���}����x���A�@H6������T����	z�
�������w�?�z�rZ��t�9
.�������!����#��<��o����~|��B�A!z�HUPH{�A!�TH^V$9�	�H�I>#��o����5����&>C)P�B[]U�]}
������P�����r6Zb��G��U�����+���H-X$��A �PHw*	�� �����?�����_�����GR���
��-��x��~t��>[��w����A��HV�U"C�V$-VD�uY�!��Hz�B���>~�������?����YA	5L���\=C�W�88Vn��6=�>~�y����:�z�$=��C�X$=��$V$/+�����V$>���������������[he�������N��Oh���_]n i�-'.-yJ��P������X~���QS���N�	�rm;��[�����S�l��lv�n_K5x���V�����gmpQ}��`����`J5��y�l���SoY6�7s��~1��
dz*i����yN���3YE������9q���%�L�����2�o�wS;r&wG�=l�F�o��������-��C����	���+i ���9"��,��C�������5�{��^��nvn���yH��;D/t��-�����6�AR���)/:��Dd��z��a��[���k�L��|��E��,��Z����MM�:�/�P�i�3a�e[K�A��g����$�:(Yf#����'���t\�<�� g����1EO�;Z;/����M
<�V^1�F��$����B��K��x��A�<4�P�+8$	�I�rbI-��<�=Y�����������z��3���pz�jl�>��j#��Y)d���}ZIbC�V	mR$V	��A��C�`�UD��bC���w����d�GNs{�:��5`��������~�=���a������Aj��� ����C��H<��`��V	�$����o�������o�ts��%����]N��������)F�yt���e��e��	=�[4�W�����bC��B��HV�$;���$-�D���$/y�_���o�h?�J1
�nm�N��Q9�������\�L �~��t�����R$=�`������P��H{�H��X	
���o�g�!O���Z[���V��|	.o�������o�/�>�a�B��!�X	j� �R$/*��R$/U�A�A!�TH|���{��o1�����m�o6S+�uv�������dE<`���)����m+c6V|<*��x�
*�C�R$+VD��A!z��� �H�mY���=��������l�:�kjK]/;zE�]~zu�^�����_�w�w��=���C����U"C����C���A!yPHw*$-��������������������bS�v]�t_j���
�#w�E�<������w�|��~���C�R$+VD����j�C�V	j�A��B�D��H�������������~o5����lY��u�H���X��c���h�m�M<�������_Q�I��P���j�H-PH{j�!�V$-�$9�$��������_��m�����W�/1	�~�����X��X����Y��Y������}Y�M|Hr��dHwU��D�5bB�H���THz������s����LWB*�u%
/��W��}Y���C�������/Qn�;1��=�Rz���
f/P�T�g���)�����ec"M��Wly0.ao(P� 0X�|��gw����:��2_�k����y�b����e���������	�N�C�����H���zEi�C0�Hm���S�33u��xz�a�!�y���������Tlh/�0��<�����wC����nH#G��}�*��H����=�x����c{*;���u������+w[kM�Q�jL���_�t�{7��b��Tj��X��:�s���O
�R�/!�4��mf�swfp[��C����e��J�1�R�^O���lX�-�kz����:��c���J��`s���������;�"�������W��J�1
��F���+I
������X�p�+hs.�Qs��
���a�lB.9�*�]�ZZ������lRt�n����iC���"=�Q�p��p��V0�V��!��A�}OX���,I|�Z�HUX���$V$/U�� �bC�T������|?>w����&gA�*!��&�N������%��w��|���=��~��~��Aj��A ��H*�
V$T��H{UHZ���w���������}�v,rSZ�7��f�={���e{jp����u�}����'�����
�� ���U�C��������	���!�TO�����=�������Wm�)(y�tU[���P���������=���=?~~�=��?�V�A�A �D�Z�H*�$=�R�A!���d�����?���:H8�R�o�a���c6���}_y���������~>����=��w���?�V�$VC��C��Hz��B��A��$PH=V$>����u=����W���7�����*\�/�������R�	�+��N]H��W���r}R����	��!��Hu�	j��`�z�HZ�HW��}���/������w>5g�/ip)m������sq��p�h�5��V&���1��W��I��TU�B�dH{U�U`���"C����V	��{����y#��~�����z���:���?n���������=��%�A�[��W�3I�_T��`���D�U"B��C�TU!��!����������Z���#^�r
@���9��L.���%WN���1��p�{����|<&��<V�H-X�UbB�PU��bB�� ��!�?��>����%���N��z�����N�#������u�8X�����M������|��C���Hw+��T��bC���HwU���V	����=����������Z�w��4��@ ����P�m�<�kG�G��:�Z0�zP���9�;lp1����N���^v1:��n����j����g?y���x����#���*����r�o�g{�]W�yr��O.���b�'iRK
��M�p�+�����#�%f�W`.$�R����f5"�n;=�<O=���'!��z(�|�=���Q�(��xu�/s�i6�����x��/�	#Tv��=�
q�����-��^.�{��()������:���x���:
���^5���6����W��Z�%�	�l��NE�|#b|��
V�p����[�"c�U�Frdf0���w���]]�K�x��,OlC3��Wd]�m�+6�{8�C\�9��Pho[{�����x7��
�4�>���	��a����$�:��� J��=���;|�u%�0���vN�$y�h�@�{�����f`s�i3���f�mS�}K[$��W��n:u\	�����y~��2(�=������"C�TH{U�C��B��Hw++"C����$x��/���q���������W�{a�v��r�{s�i(�yM�u	i���C�V	
�	T��z�	mX$=������<���n�O����Doom�}pN$ie~��.R�|3]�
��On���Gb=����A!j�A��HwU�5A!��	j�!�UHz���������7�b9q�����m�����d�q~��=�a�A�q�YK��|2��A�`��THuV	yX$/+���A�`������=~m�����5|�
5 �	��{�p{�G\��7B��l��; z1�����!�U��A �A!Z� �bAU�C������Q!:N>��y~��mM�����H�w!�S�1�x=��;�EKyk�;���T9THw+� ��VA����T�*�!��T������~����/hDyf����{����c.�`�
������o0GZ���?��Z�
�"B�bC�T�� ��H*�!��A�C�>�������}���dy;���0�e��S��8��z*�ww�F�r��O������~|�
T�mPHZ�$-�j��j���dHw-!�� �+�_����v!N�
��/���nu�
�a�O����o�H^���d?�*�C�����j�U@HV�������
T��y����������P��[�3���������r�fd��P|��~��UbC�� �H���"B�bA��������Z�Hl���[��/��&d��XWT|=MLk�������i���o��V��+{���@]�:��U����N�}���e�&���l���/o�hN"���'Bz�����j{���x�M���U�V����0`Z3�P�~�:}��]���� ���������evJ���+5e�]d���b�)�{���H�����	�����,{��
���F����^����E�D����!�e!�=N�FT:M�P�o�%���b��o���ml%�fU�._����v���)��V����VLm��'$�{7{*�3{tqha[
#��j�W,�������mEz���b<��|rn�P��_l���gS��z�2��L��ep�i�%�������b�e�
kU�t5���'��u�(5����{��������*��j�<�z�<kvYV�,w���j���yTYK�eVe�k�1MI��c\���e�
�z8�7ty�����oK��=������)����gw�����'�y�f����G�	��#.��5D�U"B��!�R$-����Aj������w�������`������>�h��n]l����]������M��P�3�u�����D��dHz��C��$�H[VD��A!�U"C��C����7������p�������28yv��������6^Y�c���%�I�t:j��q�U�A��HuT���$/U"C�X������$VA�`�
��I����x��*"FW9�E�8��/�xP
vL����������W��R$-T�C��H[V	j�B��!j�!yQ!z�����)]8�Z�]���$����M���W'VEF5�Cu�n1�����Q����HUX$;��C�����$:���"A�@H_����?w����uw��~������Qan����]+cJ!QL=��;A"6+�F��P�����<G�U@H{U�A��$;��A��!mPHz�AU�!��{������;�J:���p��]lv��l�[L�J}&9�0�D�����|��|�
���T�j�!��
�	z�$PHl������������SH]sgP��;�'�r�}r�>T��D�����M�7K�����~�V�	j��U`�������!��$9T�B�`��V	+�o�~����~�o�:d�����zwp������h>gb{kb��x��+�_��-���U�j� ��
�	�bC�PU�U�Awd���[B��K7p���A����W��N�ak(S�J��iV
nN�|�|�'����R$=U"C����j�C�X$-T����G����qu.���#s*NQ������p�]t"��b�@[�q
����{@}[N�r��!����T����e����K�	��Us��1�����AlM��D��V�`fVR��y�����o5��=�TT�3^H��yL�%n
X�����GR��,{�I�Sy������{�.��J��m�u�/{��NdM��U�xx<]O+�Gl'��Y�M�Q�pe��T��\�<n�J
�v��{�u&d���,�~���Ftu�J��}[���W^�T�������W[]�C:W�^��m	�A��P��A\��{��U��C������[y��bv�
��C%��xNc�4�u�$�j�g�C'nq~\<x'�+4���t�=����/�n"���)9K�M�PA�Ie����N�a�����Z����x�UE��O(n����Z�yZOQ������}z����.p?#��.�]�-�����.Y��P��p��}���>��~{�~~��������/+!mX��� �����`�z�Hu�	��A��$?}��������?���2����hIq[�yW�{���{NT_���v������������H{U������j�!�V	�$H<���!�~��^��m������W?AOR�9�9gkk��y����%Lv?x���|�}��v�����	�B�R��$/+
���"A��Hu�kHw��!������o�t�(���0
 ��}�����<����������r�T������}�|2�!��bC����Z��X��X���Hu�Co�~��y���>���O��t�c�]l�h����\=�H�m
���T�*
��9 O�;�������HV�$:������	��C�X$;��UdH/��=��G�y�����'�l}��f�$������~#t9z��&�2���#����|�������C�VD��THw+ �A!�V	�� ��G��'�xuXs�tAp�}Dn����Pe�od��F��H���8��Z������q?�u�"A��B�PHz��Yj�!��C�X$-�@#��?U�";����6�����;x'\�����,�k�n������
�����;|���C�`$=��$��H�yX���
T���	$�G��*������]P����p�������L����Oc%*��hmd��?�V��!j�Hw+����T��D��A!��G�����r_������F���}�<����(t��!�9r��$��k7�mb�R$+T���A�R�D�r�$;����V	�dO���;��ym_�����K	g^�������t8�V��s�7l������
�7EB�	�D+��f��;��m��:��I�@!�:Eb��@2yu*��6j���Rb*�]��15f0*�����<��������EVV��H�
R�^���t0��l��Q�:U��~���/S�Fq=h��_vM41T8������a\�sn�����j�Ld��(xQ{8�
������q�������0���n^���)["����������[�q���6���v����`�&o�OZ+�aE�@T����|��-�����i���}�� �n�O��b�������1%O�8}@�4�z����y�.���]��g
M�-�a�N`�{���R�J��J�#b�g�Q��w�A�q�����q
,���q�����N���W\�y5f0�y0��G|=��]/K;T0]%o;P~�uz��%���sp]�P�]�������������,[���}��31�U+$`����@�[Uv�}���{���������!�Q �Q!UA ���H����mX$(�}�G�U�G���Y������W1Y%�Yd���;�S+;0�������n�����U�*�B��$/U��j�H6�����A��G��BqsdO���Cr��<f-b�V�WJ�8g��Co�\6��i=������9�B�PH=T���HUX��T�Z� �P��$<tG|9�&d�N�UvG�*��6?��x	���mP>�7�[��/~���B�A!UD�r�HsT��@Hz��uX$=��>	b@!�������+f�~n_=�{U*��V��������U�^s�/*y���������������� ��$-��r�HuV	�H{�D���H5X$-����q����k����?�j����Q�G��L���+���K.�t�����p��
��`$;�"A��H*�$*�T�5d���������Y������U��.�w";cS��U�4q�.���)>��~u{�����O�
T���mX$���Y*	��G��$<	������f�w-Enr[��������)���]�j�1v~�o{�a�Aj�!��D���
�	���j�j�H=T
��C���f��u��m����,����]]b����/�?�������Bs�>���l�!�V	
�H[V	
�Hw*D��"C�������>x����+1C����o�zwYI}��uS�V�]��4w0�_����!j�H{�� �A ����������R	�H��������9RD}����������,��o�������������	���^��fDVOj�����7$��{FT����*�^5M�Yg}�u�|�L�f�0��0TY��yx���������:�f���6�T��3�h3��T������.gG.�.bY� ���������Uq5�{C.M����0:@n�wWq��!��=7N�\,+C`{��J�t9�de���#������t�]ih�����"�3w�g:4w�V{�5hZ�]K�?*	<��<�}5�:�A)��|��K�c�:�gZ��+�����!��_�u���w�������;s+�*r!�j��y!��8'�t��G�s�=4�^���@�@]"��\���������ec�Dl�P�]���
[���o}��@+{z�����X:�l�]����v���;\����]���{-�;})P:�����rv�0��%K���Z�f������P�N�[/DS��p�Zy��V$9�$=�@HsT�B���r�HUY��:��+���|7�L>���B)��0�z�s����jO�^�}��)<c�~���'�U����$THu���`��V$T������������w���z����4������k�s"��/�������Z���W�[��yY�D��D������Z����D�5dH;~�~�����<,�\3����*���E2������'�oX�������}Y�X$�����Z�!Z�HZ���!�U�A���_���>����5?^gQ���[�h��
���o�����<�=��e���!�V	�`��PH=V$=U�C�T�U@HsV	j�B�����z����d�_��7�c�`�{z�?�^�b�}�A7g#a&�s*=��|�C�>11!mPH^�!������U�UH{j�!�>��>�����||��/hD��-����Mb�5\A^��z��+(�n���������b�����B��A�R�D��RTU����������{����AU��'$m]e��Ia��I� T�������������C��)�����
C�������hR���h<*���b������r��f��]�s�SBJ��@�����~���\f^��u���*�yX�����������A!��H*�xw��sK���>�7FJ������vG������8/�
48By���:Ao���|�?�-PH{j�C�Y�R�C�V	�"B��	�+�_T�z=��}�c�E�7(��U�������;�P��T)�C�����o�3n��>�t���/�hN�f�e�WV���L���,�$�$P�p��{��7��q���5� �(�OuF��������m��#B������\��A���������=����.n1��w�����^nN8R��~X���B�����{�"$h��L�{���3n�;S����^�}��y�R����}��]�jnB���H�Z�����36a1�-��m������Uj�zz"N��9��^�����y���W�hlL6�D�2S�]�+{���X)�v��O�T����1I@�S��A�v�n<6~���%�&�0I���,��s�8w�m��X�K��-�����y^Ev��2�H��Pv%e���0pu�4*���[[����P�;fj_����a�������ds42��?V�{}dY�v��;_L��.�&�Y	��30�����^����@���i ��oL����O�����r��
����Hu�$�
THr��x��|�rzyh�7UK�7:�VN���~����{S.����|0�v+��yPHUR$*�Hw*$9�j�!��!�+��<>�bc��7��xiTv	�W$����~��C��:P�X�S~��S����>��=�dHUPH{j� ��<�PHw*$;���`��V	� �i�cb��/)�a=�?�G�=�%�s��T����}�S"�����!mR$9�	z���!j�H{j�!���$/+	�>��p�z=�['�3*��Dj����f�D)glqw����w���_R�H���$9���V	Z�!��$/+�5bC�X���������?sSI
��1�����Q�y(���eM�l���0H�;�fR�����6IH-R$-TH{j�AU� �X$���!�T��O������U���X����B����uk�E�*~�����';�wn��}E9>��UR+"B�P
T��A!��	U"A�R��o��}�y����62py�ymr�c��h�YW�
�@]ckN%��iJc&�����j�
�� ��!�TH^TH*�$*��yQ!��!����������g��1m�[���������?!-]�C���;b��F���D^K�0�������C���A����D�Z�!UA!���H��U!��������������hZ�������gr)�b�s��r���uqr���:��L�~���T�H=T�UD�uP��AU��R$>�������~��6�Sa�A��@��p�O1���ze|�����^��{��<����V�hE�2}+�����:-{�|������DmC��uC!���������TBV�S��b���_��}/}N'o�����}�x�3�9�T���/0Lr��������.��zMm��q�k�h���;e������<�������}��e�K3xF��t���:eT�~�[���|:��U_Sa��4��������X�2jBvv�q�
n��=���mu����V
|��������Xz�V�n�0=�E;�:���,���]C��\����gQI��Q��=����������%�7)�Y:�]�������[�,$��o�oj�R������?5m,����u�r�O;�q��M�e�.QN>�V*u��n��e��p�=+5i<}q���^�4�9��]j���^�P���E�����-��P��Z!<|��.%����;/��M��u�By���+�9%,�mR���������D�Z�Hr�
�	j�Aj�������!��"C���z����~����y$����.rX�����U����������m�{~����!mQ �X�j�
������Hz�j�C�������>[��1���kN���WnJS2����]�������{���������=�D���H{��C��H<���X$=�H�	$�<&� ����&Xo����t���
�J������9ife���������������?�^TH^�	j�HwU�U����!���<.��G��k�����^��.d,�(����6u,�QkS�
�&�T/��]��{�G���~��R$�$/+�j�!���	��6H�I#�xNdj�0.e����=�XJ���D��z�J�-7�W�� �w���x}>����~O���Hw*$V	�A!�X���j���G��$�N|�����Ga����m������f��]��u%!/v����hB�rJ��|<$�>xQ�`��VB�X$;��!��$;�� �����D��������n{���R���g�������ul�<����Mm�o�}����`�A5y�[TH^VD�r�C�T�U`�yPHsT��`��>}�������������\
��q����>Lo?
��^��t��T�KW���W��I���	X$+T�@Hw+!Z�H<�$=U�Z�C���������<����c��c7gv���E�\�����-Xl��o��4��_��M�_S)��z�$/U���T��������PH{j�A ?���i���������2 5N��F� ��Zu0��`.������l�"���x����c;���~���]�����{����0����y�:�u��b��=��kC��EBy����k�tXQHUV*f�n���,����{m�����}���
�
B(����S�?I���$*+���z���_�DUVA���<��t������O8>���~nE�b�y�8���G�g�{6�m�=N4=��y�R7GR����m@�����n���_
����� ���=m;�N�����:�}�{{~�UR��0*,"V�o6,AA���<���r�����{����?i0xaQ�97S��E���,'�w>�����EF7����{�o�0���)_9���B���*���Y;s>~��%�����������o�KS���QXY{��$t�$�}*g���w}���i1w�gpF{;8����#���Vf�&)�t�,���<�X��u~�W��|���9���>^}��fe=��/w6�~�FUQ���	��L}��p��0��Y�5���_��,*����Z�x1H�(+���Y���B�+�����ca!XFS��-[���(Px�4Q�DW���@�Y��Z���S<���=e��m<�wc����v����d�
A�"�r�;&�w�:V�P�%���(7q��a�����|�F�6�]����P����������
*������[k,0(���#�r��f����s����"(�*g�Z'�b* ���#�9����0��+�r����5Y����">�����
*��"��^�m��)*��}5FX���=����M5���7�4�������ZN_:��SH�lnK&�I+0�?9�\^�wK��k��=��7��4����1���%���q{b��^��]_s4��.E��|�������������"�"�5�x�w����,0��=�t�����{"*0�9�|���-�QIO4����*��.w�vp�a��Ff}��[l��������/0�����&�rf�������{�WilR���X�0��R�������`t��n�Z���/dT^�8��O3���%bX��@[�}�
�;B$u=@k',���3�~��Y��u���������r����Aa������7q*��"��~�-e1AQS�\�r����a���������%��������O�����XUH{�5�����B
0��Z������0���
("�I=�{u�d�1��N{f�����L*v�	�'-nob�n��x��
��]�������/��tFcHV`��}k]��1&2���-�����`��<m�if�(�v��������}{������rl�ak1��z�A�|W+�}��<�������eU}��#����}��}�{��XV!���k�y�
XXEVQFf��%TR��~��N��� �.6��I����L��U����"}Xo�K��M;'`���Z�ec �1������M��S1,`�km���:��Wp��;���=��uH
&W����&sO
��
T�Up�U��!�G�~�1�3��*(�z���O�1h��0��������
���_n���"�TU~��w3�3�� )���3n��H��
/��=;����(�
0��j���Lo�R�7Y�WBc��-�q��R�����f�oM�
�%���[��
�����;��,�IyOW]���R��< ���xd����Z�n�5�l\G]��E�1�������Xa{���e���0��	]����iXa�QQV3�Q~�(��EU+���|�y�{���(����{'|��J�
��a�2N����)��)}�s�gI�HPG�K���#������w�f�g`�-��d�gN�����R}��I��w,�f��U����0���,�/�[�@�w@Y�^]Oo�����T26}ih��r��8��m���
k���
N�1�O{����HA�A�[�3f��R`T�������JDUG��JV���PER���Q�sY�w�*������~Z�����>���DE.{_3��bUUQ�������bETQ_vw����H�@Si���[�n������*E���9�
�DD&�2�+���9R�������:���TG������><_��Oc&�Sf����s�s�����H�'�exa�VON�ox�7���D_���x��(��*���������QFUQ����I�{60����=�9�o=���Q��_�������� �������!QQ�&8�m��i�{9�t���7�^�U�)�[:���rX�q��{�AJ�*e<U]��Z�62
8�J�:7 >��9z�J��5D���qG
�������0M�w-w���GbG��c�����EDQT���}�����(�(��eh``y/���pQQV�e��ru�a�Py���_M��
���w�g/�T��#
G~��u���X����N��h��rv�J|h����7e:To:�I�������t��Kpt����/Wy=�emp�dlM^�'�S�z~�9��������h"�����P�����>{��=ztQVDI���
�������\NbR$")
�^}��y^������P��n��r���
�	wWK�_����sUU�X�L���y&�(��B8� �9Z��e������K��_�@
E�EDQ�����fV�s�C����5-��97w��B����P�wb�
��������u�_\����UQa`aaV!EXfwK��	9	>��
|����go�C��cv}��_3�v}=������mnUl��u
ATXAUE.�Sk��;r�X�����[oV]�������*��4t�{��}3\����{��`���"C��2�z��e^���U���d������P�|�0�� �
"S�N_h�VK���{*}���w
��J*	���"*<��o39����u��v�3�yWYen��w�����c5���[Y�)<���H��?�{��lys|���cDVV�O~^B9��<�t��l�}Y�VJ�7y�����XAU[���Zg&�AaA���3�h"
�F_���{���w������kYQAa���l�O��#�B����o�N;��*(�"�0����~���G�UAEQ�bN2�2�]K����@��""���
"z����s��S��uew��<��
"0�0�>i��'J,�s"��N��f�(���"�������>�������r�Q��}�f�JTETa@�s$�[�������J�mwb���"*����,+v�y{��j�{���s7�B��0���q�{�����(I0��
VAa�E^���hvs����(��U�j����Y��HQE�aUUz�h��6��ESR�u�^��U���Pa��o|I��6_v���M�U�9���������0�"(�{����8��yZ���u��{��b���5"/�����#�vs��+�]/b���k�x(y{����y	�����ol�UAyEB�N�Ah[w��������W!�����DTU&l��{=����a'nG�������",*���Y3�N+��*��)��������0�*���������AD���w���Q���G;F����������$���9�����D� ����������4���/-�V������c�\���(�{4����9�������fep��*�PQUa�w�<T����[�k)��jt�'�����"����(���������=���������qXRa��a9�O
��_-�R�r�3��M/����)����NQ\����]�{��s=U�}S��FE��EXPc8������z����o/��{9�m�=�uU!�TVA���= _Si��C�����+�]��8���������n��u��n������n{�[��m�EXFDU�
�r]q�6F���,��)a�eo��6���������'r����]yR!���.�p����?*{�~WFU��f�k+�IG#PT;v��C�gw�o2~���Y�QQXAa�9���AF99�����H��
���7���Wa�v��0������9�����0���
��m���Y�_���s�T00�>���4b�a�E�����8�D����������������QRXUU�C�s��~�*�1/�^Z��e����^�5�RTEFHF;w�\�B1�W�Y������iTb�����#W�<���U������1�������r�)m�
"����7n�������QW��ns;���d���"*�$"����m����iyGn��O4�m���+}�U��9ra,0�
� �+
%�4�n�{N����P�]g;M3��a�AaS�_y��d�xF��]Z�������)DL�S3�=�c5S���r�o�f��$����*���*����>�cc��������Q���F�
��`UUB��{������0k�I!.��V5y=��Z'
��{��".�'����@v�>���W��HiY�n���mc���[��h��f�� -����<��]�d��3;
Or�~�5��������b�L*�*��6�s9��	a�XUQcG������`Fn���}��}����{;f0�"����6��:��c""&I=�q���VTXQS��g�����" ���k����!��Q����"�?txr��KSZ�q�:��1l����>�J������{�}������s�q�������a�bXV����v�m��;
�sD�V��������aU!X`\���kZ{��$�g�M����x���EDEW���a��k��k=�k�s�o��{��+��~���
�(���wW\�`nP:�F�CCH
��P�FDXEQU�f��W9��gl7�e���iu��OhT�T�aTUU�{��zwdJ�������ww���9�Q�XQQUQD]d�O$��\�8wy��f;Quu^����@���
��	������d�IiM�E.�(V�Y��2�[�����\�H��5���^��
^�������d�]������H��X����5]��z�Dz{�WQ��a�y[�r��TD������r��EF!Aj~�����
�0+��k�y���>da�X�+�U���\��*���1�{<�������
���^������U�U�I>�s��vI�{��,
�����g�6}�UI"�*)�������3���M������{�[���w��
,"0#��UWj�����im}U&�}���y�x�*0�
���|����Q[�.J�s/{B�0�
��W�72F��,S��*U����Z��T�nK0�*� �"�q�Uy�9q���'wu����Y�����������
,*Q�f�g�^=���������Fx�{V��rg����\l*( ��-���wqT�99�jq��"'�e�{�[�N*B�0"�������9xm�F#Iw;s�����(6�SA���
�,
,{��S�~������v���s���/�L�.��J�0 	�p�a�s�d��36�po"��SwzT=���M��#c���G�+�� ��b�;;<�N�
%�Q��P����fu5%��k���`OEt5��;/�g=�"��,��;�=��c�B(�*e���}���bPDR�s���A>���'��X�� ��������*���s+�EW�u}��)�EQQXE��c�:�*�"�3�z���"�(���7�~�����<����������{�NL�������F�DU>EK��$_t9��8b��0b�C��+��4�TU�UX{��Ni91o<���M�������D+�XFQQ�Q��r���A!q��s:���`��T*���B��"*#%r����L��W�k����s7��+�GQbE!QUQ��S{�G+�UGE�Q1�+"L.�/��Ta!#{���t�i^�������[�v�������N��B"��?]]]\�0��T�Y����h;Z�MF@��� *������K��W���I��3&;�o���(
��P��K��)uw���9���x�a�9��O!�W+^��Ut��hGT�VO.wu<��Xm���J��;��9{r���m���q�kro��,�y�*t��o-��Y���;����EVf{�;^�_m(���/���{~NT�������{}��*+���O-���iJ���=��������	Tc�
�������������aDQQ-�T�s��^�=cDXXTA"���������]oo�z-���(��������
(�0+	��o�9���}PL�9�;�HU���qK
��UEUDS����w���q������5�q{��u��9=���XZ
��Kh�;%+mQ�-����{&/��EQUFTT�3���wb���5�E	�I����UQ`Ta�����.�~�S7���OWj����fw�G#2�{���
V!�FF3.Gy���9��7��u�zr��zjy�d������DaA���}C7R�gnu�z�<���fsR������*�,,s��&u��Mw��7|�G�9��<������"��{�6��.w�w<�+���a����u9R�1��N���>pe���AQS�Jo YYKc����}{�2�P���d��F�W����������)�x�B�fO�K����X��a�����Wg�I�DQaQ�DDFvv���i�~�qd,*����������7��\D�XFbD��g����#
��0����g,�_m �,���������`���U�Sy~�O��rS��
�"�B�=�1�TDD`aA�i��g�+����Fi���h�c�$W���w3Y11�p�(��R��@G�M�����
�����%	�(���n��\v�P�������iFn�X��� ������C�"�w��w���k���3K���F�3o�:��t��X�d�yV���C�,�f��*�8�u�KF��/��m����:%�sV�`9:UM�f]\��������kg,������lgR��=�����c9"�QWuc�"����]���4-n0o_B��U@s
�M�3�Tu�:8�<��,9vi9�-G��%r�[v��~�=HJU8�*�3�/�Z�.�;z6S�j#W/;@��sO+���VE��
�L��kO���x()�b��56a�������K��!�o9.^NJ���5xm{E��8X����r�7������zi�n������%]7�a[����LD����i�61�q4fmmr�:9��_,'��Xz��Ty7\� ���Y[D�Gl-��,[������"�xH�U�E��/2�Q�{$E>���n���`@��R�K�+0vm�]p�e�l�p��z�Q�%l�Q�v���f����k��{�e��2�@�v�q�P-�'��n���T��%��
a������
[��]Y������������vo���t�T�dt��x���L*��I�JD�1���9|�Km��Ht��������2,�o8��7��h�-5f`���;��]�qG	��\�W`���	�����`�CS/���WST�v���,�;�H����[Tr���d%VT��T���$D$,v�����Yy��l��*�:�pXK1�x�U�j
��H;�P�R�����wMK��L�9���F�sWW�"��/{q�Sl���M���"����o�Y\E����������u����/&�������C ���A�kK3a�c*�qW9�wf���f�l ��!/��DY����v:i�
���Zt���;h��*���yB�n�����//4sC;g_tv��\@9[@�Gg�5������G8�N�%Y{���;���\&�G��t�	���� �Q�V����*������]+��vJt�U�R�#��4��[�&I/5���=���Y���QU�m�M���n�|$|21���z�=��w�]Y��<�k���c��N���n�{��1���Z��G�:P[w`���]�^���w1�R�b]u���f����0�Gh�zu ���6vk�m���w's�c�����GA�����}�nby�#��V�g)���,�n����q�|��Z�b�����P,����v�^���������a��:���p����7�d;����x��E���t'#K�5���Bt���9$�,8���O�U{���"[�A�q�R��uQh�x���@:���v����Y��t����������O�n{��l��r	Zv�^��r����
�������@��b�'v�<w3`����e�8�w��X`�F��,�w������((m�����/"9���i��2���:�� �&������n��J��v���K�z��|*J��!�0�=W�r�f_,�h��
,6��
Y���7aC���'Pw`l"c��z�y�j��U���3�l|4��Z��R�viMd�I�*u �.[����!����Z����
 53k�Lc�[������&/��3��&]����q�&�Rpok<VqOx����0��n���^Ck�|����i��=���v�T$-.�0�N�k���D[����a!n\sn<o��R`<d�,����}}�*�����YMhYY�l��mF�f�,�	0��c���Cb���r�2,�g�E����/���
���hP���yG;.�Mj��P�CFKO��\�y�vs!�����{1Q�����LOP�>""����k$����S5��h����9W�182�8+x{���f|�s�[{S�7H�
��wL/��TX�Q��b=r�a��w��37���t�<��`>}H��(>���^���j��{��+z�\v���#w�����N}��r2�J��	���w��w3:l��nR�����}�&;���p���kU��(��6�e����BEE�sG[���I1��/�a�����5�����y���H��,�n��<Zt��#F��d���.�}}\���7�[_,o�;��5fr�7��!��XNp���cIxi�a~=����g���<��f����t���9?&;�'��2]^K�HLb{�hU1�=������
Wn���{����!�0���U���l,��#����S	�{�����B�B5R:#�K����V�]WE���xn��V9�?k}9�
�pD���e���0�����4__L4����}vy�|�"Zb���
`��}���g<��k79��p��l�\�^2�Y��}���f]1����|�-�]�����a�pV����Y%��YG3!u��ei%�M=����Rk���u{��ql+�Qe�~��$,m���Zj����OuD��w
e[Q��5a��S�9S7;4Bv��C�*�x���������o�#eg@9*v�8��1���k���bVj���s�x����Ud��z��C��������	������`��VD����������k�~!_X���*�BGhMo��!�p�N�J��������wu�_����B�X$�H-PHr�$�	j�A��;��!UD�������m�������Q��D��M�8�p����-B*Y��F����T���xU�@#������
T��`���"B�X��X����>���������6���L��<cL�M�J����+�����6�s|��,8�W��}�����J�r����$=�bB��!mX���$-��U ���o����{�f��VI����G#}�������t�q��s��W���fbA�I �A!UD��
�"C���
�	�D�j���������������'�y%L�9YU��4^����s|�����������{�|�yX�U`��Q!�X�����P�� ��$9VD�������?�>y���k�����;C�K�_���[���A�eO����������C�THz�
��*�����Z����B�$<>�����_F]h=�����7[�����h?����w�n����c��}�?�5R$=�dH<��C�dH6�$�H5PH{j���!���M��=���������#e�-�|{Pk��������Yw�������T/+U �yQ!�T���z�H{U�
�D�j�C�������b���zNu�������:��\��B���pX�u�Z���}�A�A!�����!�Q!�Yj�!��D���9THn��������W�(`�w�h�[����65n��S:�����I#���������
����s���9�������Ue:���
��jgrb�sO�c��&�)�\L�'��T�{N�&M�2���W��=����sz�*y������M�S=�vvp����z�z��&�ih{�z����������"m����
g@���|.�V����Dz�L���}1�{���n��3P^^��<Q���L�u�%'�_M���|���wo������|-@���[���/��(�h�fMp��H��`NW^�$��x�?C~pp�|lCg�T5#��f�����_P���y7��I
��oSC�X4/��i�;��o�[����t{�]qq�->���;MA�������t�$���z�/��
���*�d8�^�u�p���S�����J��������9x��er��X����R
��|/pD�1ti��d�����������d�Hs��N��������~��|s���yQ!�*D��T�z�� �����Tx�I��uVH�O���iD=���_g�i�x�-4�f�bp��8���^cxJ����z����>j���~�B�Y��� �PH{j��yY�	��H*����}�������<������R�Zk^$�G������K��hSvv�l8�q=k���W��I���5dH6�HwU��PH=T���U`���"C���?�o�����Xu���Y�na��?F����5���Q�U^���x|<$�@xI$����C�V$*�H{j��U"C�bB�bB�����������|������R����w�m������|�;���xm������O��bA�`�Z�!j�H{��!��HwU�|���G����c���6�n:����� ��L}�<�*���0xgS%������i�?���������PHZ�
�"C�PH{�bA�@H^��5`���Ho�|����������}��V�83Ao�t����o��ot�AS����{�[�_~}����j�HuV	j��H��P
�U��������w}>}���>{����P�zQEe��+��wt�l��]f&��i���/r������?��HA�A!������� �d��D�j��_������/]^�+�4r�I�v��Loe�9?6���C=�$�6qQ����r}R���bA����X$/*	����!�X$X��?�_���u����~}����&����g!�s@���BN��MD��
�]��i,��|<.��x�Z�$9���D�����THZ�T��	o��|������s�#D*���Y�oVd}�]��Gk�aL�g���\���c�w{�e������P��������~�WP�grd�t��@������%��Z����<b����P�~�4������g`����th��P�({UlQ�D]�+��c��j,�J�<��q`����<m�`������n]fu�j��uX<)��s���a��_�H3��������qq�;�N"E*��^�������"O��M�mk
�k�����
����
�~�a�����v.l����|�M}�����hD��{f_Y5�:��J��:��:9�H���jAj��EI�`������{j��h�m��+;sgn�z3�+Y�p62��Z���y�N���E���A\���2!��%>�o]�C�v�q���,[p9��lO�Z'N)��qC#����[g�4"�����W���C����*�uMx#��(�:�+���%����D�����������&SB������ ��!��H-PHr�	�dH5Q!�Y�`�R�s~��K���.�u�{�����d{��V�M<m�
�,��j��u��������U`��PH*�HUPH^����<G�����������a�L�������w�G��x����$V��;o������!�R$/+��H���	j��j�AU�������<'���<�;����='��*��em94}��9Y���}B��zvcz�i+��~bmYU
�"B��!��D�UA!z�Aj�!�W������O6z��}*2�x*F������.u%�%CLr����%��u��=�D�r���!�X$V	z�HuV	��H5R$?w��YBo���������f=��&*����-�T��������vqo�}��T�����uX	��$=U��`$:��C��$�����5o��� �A5t�G!�K���������7���K��p4uB����v��v��9v���!z��v&!�v��v���s=��)&����l���5�����O��/z�vwS�.��7wSA�O�.��n�~O�������!�vA��&!������LA��1
����Vs0`��Q������btM�������`���H�~{YwS�R�(��������Y0��W����_QD�1��1[��!��&!��I�=]��m��-vC��O���#M�<>�{�z�N���6�m}�6�b���vA��%6n�f5����?�f-��[]��=��Cr�!z�LA�m1���;W`�.���=���Nx~��^Lg��J��X���Fh��Fl��$�t�xe�b�/��B�����i��\qO*�j�>�;
��%��(��9����p
�����5]=�mnc��c�Z/�k WA�s�����Jw���J1;����;��l>���h!1�N�`��H#���fe����{�z�����������������/����)x���{���i��M�
�;����8/���;�,
�U�A�J�4��xc���.".V���s���������JX������x����[��*��+�e�w4�T�lg2�G'��z*l��YN&M=�����fg�����%�BY��w���)�Wg�������gs��Y�9�a��sm�C��`8�k_s���[r^�u��#b�]8�%�.g��=�P]��>��)5,�)��
��������V�0Z��=��N,U4�9��L���%Jy��E��%���k�5�{����|/�����}��?�=v� �v�7.��=s��t�/.�r�Y����n�3�<6�}�S�Gn��j['��h�yK�������S�U6+��kv�����:�5�$��W��JH���1{m�!WmA������l������{s�1m����N�g�
�%���uc�������O�g��d��HV~�����yu_����� ��bgh��m��wWbb���Z��]�b�i�.����������Cy� E'��tCn%����R;OgKw�H���/Lu�����hb.��{]�b��1�c������6v�b��G�-��K&�D���������_�T��>��;.hF���G"��~_�~��|��=]��/]��z��1�c���
��b��LA6Nf�s<0x3p�5\>�"��	�70;N�K�x���+�)������g�0�n��K���!2��_QZR��}GRK�v��.�����b���mv��7m�B��!�����_�������8�5����E�-��f��tm<�[���	�9;{t�����I/�����-������ev����1vv�!��1W`�>�d���|�i!����I�V�r�����'�}�^�,�����	m�')y�Z��;LA��1r�LC�v��Z��!��bbv��6���q��=���/,}�����>�"w�' �I�4i9X�9��'�@5v�q�Dc��S��"[g��>�>�h�n��b
�`�=��!�m1������1k����m��]��o�����U����S#rb+K�z�g�|��.����1�yK���>���7;hb�l��� �v�!��C����.���.�`�5�i�?�����N5Z�{j>r2�M�D���~�����^�jo*���?3����:�,	���G}�m�[���pOen�#U�=Y��*V�u���i���L^]C��I���}|,U�]P����s7�wawc��^N��r�K�����ZV��Q��,tY���q�o�_}���{W��Abj���6Z.���8��=��������9�=�3m��'u�\�@��R��a��<��:��EN�����mF���@x{+�e���Qw+�����$�;�
V��d"�/L����b���]�x"�j�5�x��;s�u`����T�Yb�2������@|��-�Tt5`�vtK�S����4��84o�]������.�x�'���*��Iy��aRW�&%��(]�9�����d���+xSf�UgD����i_vI��y��� =�<0����e��|S����fq4�D���9���t�����tA����; �r�O<�<)]��ti��k�c'��i1Wi�r��z����yvCgmCu�!��_*��t���y��g��M�+��#a�t4����zb�c��p;��w����k���'�����m��=������9���`�=��1s�C��b���|����m����<Vz�1��v���G�ww��kYx����o�����g���|������4|1�`1r����C�]��������sv��r��}�������b����*p��S�B���%W<���<����n��~4�T���A��v�������C�;C����wWc�;i1
s�bN`������^<����&-i���ejk{��������z9���U�Y�g��}�!v�!����:�c{m����b�c�m����1
%%_R��������XW�����=Bo:(�������9��������W��C��gl!�vC\��n]� �v&!Z���m�Cgm��l�`�C������>�Y�����������+1�����)x���>{�����lb��1��b
���;m�B�v1�m!�{s��*����|��e�O��/���1��!����|_����mnR��s���=�����������?�V����i�f��v�!n�LA���9v����b��1�����>�_�/��{���NT�]r�v�.o�:U�b��R��|������!�v����b���-�`1
s��sv��7Whb����������Ov�}=\V���=�Nv:0���R�b�b��?��������B���=s�����b��b��1yv1]�&!9{�A����Q�v>�_��8��h9�r���������������"���|+��f�"����=�l]1���2-3�>�)��yCD"����H�~����	��(�����|;
}H4;~���6�^0`�'���]>V�Ll�Uu�+�t
���v����/'ao+^[��W����a�w�,��s���;���&M#����������7K�� 4<�U���-S�����o���
�\�[xD���]_O���X�'|����r>������A���nV3�n�y��1�yn����[��p|'g�u�������� }@j��{a0��5�5���ecT��Na�*��A"�<������Y���]N��W1������{��#C�%%�Z����V��z7=ow�_�Rm!8�6��������T^:����]g�J�]�1)�o���E�������� q�D��f���j�hx���yA��cF3\�C�Vl�q}>O�H0�/1��o�����/E�+N�_w��� ��nv� ���3]�j�1
��1nv�v��*�3��?|����>��1����)n�5S����#u�k�4Yn����}�C�v�m��g;��LC����;��1���+]������|��y�B>:c��$E���m����mp-~D���t����/q �n}G��`�2�!��hb��
���7gbb�`�=��b9��������1��~��))�!j�7����5�Cl���(]�1�Iw�����~�?�^��n�C���!�]��W`�6��bv���}J��RK�U�G�$�?~��wMN�u���l(:�(�]K7�����+C������?�{�m�]v�b���z��1j�&!��b
��b\��D�`��W�-�D�(;�/N��{�@�u��f&���z�2��}�"����~��[�������Cs�C�v����b�i�s��B��As�����<+33���D�{�Gt\}������n`.��1~K{c�-��tC�����r�}������h�f��b��1y�LB������wm�!vv����b�������<cHh���\��u�wc��=��;��N���#�����}ZQK��}CIK�P�m��u�$/�t1
�����1�c��LC|���������_�Y�������3N��78!��n�Bd�!���A���t���W�0��U�
;i�[���=��&!r������i�u��b]�!����������]���M���]�����i��K���4�j���u�6%;W�'/�1��LC��k�����1nv!�z���;m�U�*����v	���k�u�v��-Q��A�65��v0E����Wj��8���**��O�^����t���g���q������;F�����yp|���I��T:�`��3B��yW��I�������".������[���;6�a���xmC���}Z|=��K�v(@/\������v�x�Y:�����Br���O��t�L����[R��xWw0x+2������*[xT6Oq'�]���tL�V2;*�w����n��n��}U��������2��1����D�������%���z�'�+��_]��[z~mn���������o�gu�c_N\�*'(�G2��n��4Q6�D����R���H�!�n���M�.��_wR��;+�V4�H������H�7j������R��EPI��Qu���Q��7Y{VqwD�*H��qw+��g�W�a��-_��.�+/�,�nP5�*��`�n�~lT7�S
�t�Jr36�9y�^�)R��������{���w7v
 ���� ���� �v�����=���=��b���`������y�]�V��P5�w�����w>{���pS=�����P����?~��x�|���I�B�v��l���{��1��1�i1
��b�� �mC��b�o�������ut��C/��v�<?{]���z'����Z�T��l~����U�f��b��]�1������Cj�1Wbb��b���?��o�����m�y�.����&�93�T�#�Y����^�k��z���?�k]�b���n���[]��{��!��1��1���!��LC����~�����1��&bq�{m��&rt{��;�x����W�������o����B�����C��I�n�bbk��6�hb������v��{�^~��������J����)�%�8g~T�>�m��OalxdZ�|��
}�_�
���h�9�b��![�&!{;LC�m��w.�b�`����cb����}���b3
yFm)�j��g����l���BV�f=`�v��j�B��!���;������;m�z���u�|���v�f'~�hFT^~��RZ���Y���`�+��N� U����#�a�A��1m�I�.���b
�cmv1]��!�l!�JT����@{�z��G�
#4m�z���;��m�oz6�y���
�b��|�{����i1��1
�hbglC�m�C����mCWmAW���O9���ZU}7�������8&�>7��U��6�0m����`}|�~}���������b�lB���v��5�C�;C�v�!�vA��1����}�o�������[������s�����#�U}d��Y�U���	��~���+���.����Q�f_�r�uB���+-��O��r������^�w��PU[��u��e^Vm���m�%OE���R!�
�
���73Op9�n�q���b:�GV���������A��������U�u�������$bb�%��4�i��<�Jw����Lel��<�C-�%����0�tBJ����������=�����us����h�������a����Qz�flB�I�5.CB>a�m��oG��X'06;�j���������r
�9%j�g]n�b.a�����R�}�a=�k/+�dsh�u:��E�lEJJ*��.���%W	�K2�(��^f���c�jFV'��l��Zr�p�}������<����:�����WC�%b4�M���W.c�S����]���A�':�F{$����c��z`�J��v�@x#�=De�<����3D�h#��m���-]_r�����|5gi�n�`1v��3��b���������b
v��k��W��WJ�����,�OK��7Z/vSCL���h|,��4B4��o��B���
���������!v�Cv�C�m� �����`���9��<�~�GTb��{����F��N�������&�6:�����?�{����v����1[�yv�2�!���!�;L<3/3�x-��j>,�|i1�Jvt����a�>�yU����mC�Q
��*����{�����yv& ��LC�mB��&!�v& ���\�����'3�
Mj�sl}y�
~���
{]-��m���>X����[��� )�`Wj����_P:R�U�n$���=v����!��!�glC�vB��C�v��7����������V��o@�Z������~]oujF_�qn��V�:�nLt;;����"�U1�m1y�C��b�v�Cu��r���c��bc�oRO��	�?��v�������:WB��B�(�Y,V����iN�sna)��}�!�������b�i�W;�.��*�LA��LB��� ���������]|v���Z2m+��x��tyES����jo2X���A6i���7�������:������=��b���]v��wWbbm�1�h�#����`�����?�M�iV����}�O^E�{�����f�x�� <��������6������C����]�A���/Whb��a�M���o3<0x6MkBi>���:�~.��68�#��v62�g8��W���^z��.<�&�}���)%T���)$C�v����1yv�!�vAk���LB���� ;������O��{;h==E��G��7��#��NgD�"OTGw<�5H[�i�����5�(����UJV�.�C��U
�}��p��E�I���}	Jw�����������Nhp`�?/{j
�r��\��{\�N�	�E�	+i]��jvt�c��v�]�*���F�@x
�\�����y\��=UY�}W�W]����=��5��������s������k����)�p��]5��`�88x�;A��EnNH���m��+�����o����m�_&�x	&��j����<��o=��u��uJ
NjPu������]P5����5d�"�uw�{���|������V��.���;S�������sv��R?G����T����+]d�Ls�qI/�5��,�mlK5��`��J]���9�nV��)NL��e��v��������9w�V�����I����I�kM���Z����F�-/�v�#/5J�f7�*����GE\���u��4ozl�Fm{^��)������ ��1
�c���C��LC��!�]�1�`�;m� �'0{�����'6Mv����k��U#l�������`_C�p���qWh���������1�bb������/Whbv����LA�l`���3����k������jw+��W��Z��=�3���vPH,����w�����������s����m1yv����!v�bW`���������s0k>�T�|�d��N��=Hp:�X<�~����������G������_���!��LC�������[�����1UK��_P�RU�c������`����Q,�|��8;��I����}u��Q]��D��������?�U�C����{�lb���{��b��&!��l���n�g�6�c���t~��������H�O�h��Y���G�v����=��}���o����I�C��bb]��=��!��!���v�!���!����/Wi�|�������(���IoZUJ�������:���1+�1�"|���6�d3��_V����]v��^�hb;hb��B��C�v����.v� [��/[:�d�� ���U'������<L�Uh6�� ��-PY�R�o��x���|�~��5v������`�7.�!����m����=������� ~��������p����RA&�FMt2����F1����$
}������o�y�����X���$9TH{�A!�����"B��xMY �Wd����l�	�;�G�6�_[��:���d���\�An�h�[jSh�I~�^��J��W�Q)}J������.��s]��{��1
���;��1��LC����?��������x��!j �'������w���(���������B��/�{�J��%�a��r*��������94j���M^�A���mSt�Cs-<���b:���!�[�������E��O�\���i~�7�M��)w��=�=��{�	h�[��������n`��A����<F�}cT
�L��~�\�H����������*�yl\������LU<������u!�b6�:{#b\����*�pi���=J�?{����������x�c+�n�����|����rX��x/Bh�>�s	�r5�1��wQ�M���vx��~)F�nW���gwx�z����cy�'��r���Vy��V�SK��s�jF���:��dn���*�$�����Nqm��	��[[I��������=�iJ���}��a<Kk�w����`������kUg��iW�������Y9���2�����B3�����d��q��`���U3l%�^������]Ls!�\��!v�LC��LC����m�B��1����m1�`�:�g�����k}����+�d��'1������c�MVkT��Gy��\'�nv�!��A�lLC�������.���cyv@��b3%oD}��pt��eJH�(����=�F���qn�����;����w������;���=�lb������9�i�<���=����Y��0xs0{�����������g
Y��O����]]h����������}C���~����C�vA��LC��!�����i�-v� �����I�_�����������r6d���CM���t��t0LHk��O����������Fg'��>
�n��uv�����v��v]��7m����1Whb
]�
����+�������\�U���K�e���s�������4��u(
W0Iu3���?�-�I�\�z��l����b���=��1
�m1���`��j~�����*���F6��/��0�����_'tMr9�r���4k����o������c��������b��1�����!��1a�<$�f�����N�^@����5��]nH�[\{�n���%�������j�]s�������������g�xQ����]��V��!�������Nf`��9��`�O���1�x+���WxR��BWZ�@G�nG�1��0����[�<���m��]��Uv��r���v��.�`�2�!����M�������i;�{_�t��~p��?���>wX���W?�k���������q}���H����>�n��;hb���=k��u�cn����bv�bgbb$���B>�9F�g�������<q~1;�LY�Av��,���#��>u/=�`9Ze�w<y[J��&4�y�s���� �0,""(}���������
��T�2�EaF`Q��>=����!S:3^����PA{���f���B0�/~��������F�(�"q���.�aD|�_|�-��E���q�a�:H?wg��Q���c����k�O��B�Zz�����`7�j��5WC�WL[�R��e_m���oxa������g��r�"�F`�[�L��s������w��������
(0�����}��DaQU��aQ�zi��f��PB�������QFQ��{��g����O �aE`A_���7�����h�������������Y�hU�_���������������^5�[�E��Q��]�r��4r�������]gf�i�������n�A���mfz��J�7q�i���j�;.���T��]�""�k��e���DQ]����d��o�0��"����������h,�2�XF���g���~���AU�Xa���klUXTPa_y�����G!Q��O�{c����*�{5�{:����b����`����yU�q��	p5:Y)2�@O�qZ��$TJ6{�J.D8���c:�^{��k����q}�sZ�P�<S4D�`1�Wjf�=�U�DE`��jr��X�*"��]Oo*��8������w?f|���TQEG�5�zy{;������V��9_dQ�XQE����t��9�*00�-������E�ng��%�6�����(�sL"�����I���u�KW�����p��%����K�����������ljDa�c�����X��c������LZ��j�3�����������7�Ow{|�l
,#��L�L)
�?L���Uab��o~���UTFx��w�����}�F���*�<����/�7k�[��*�������Uo�QG�/���{�z��( �+���^�.={���t������`$H1L��t�l��*�$�e���V��F72��.�DGN#��+����6y�hY@�Ub�O>I)�c�	������o.���c���{_{��/���������k����h�*�	&��������0�0���l�[����(3>f���v(DTU�G�rN���)
#
�(��fU����0�S���������(*�T�L��o�f��]���$CB|��hSUy�?zdG�.���WB$nWT��&	�@�B���N�i����;'W�b<�	dlJ���r$Jd�����#Mq��q�����V!��=��;y�����a�To��_x?}����"��������
,0�%��NN,"""B���3�Mf*
�
�|�����y0�,(�/*|��U��"(����L^1�%��I�����r>�V��SX��]��V7�N�ox��C<uu`o�)�K�O������$�>�1N�B.������Y4@��������sV��&���~WOow�,�����TT��>�����f* ��x�7���k���������QF;�����+(�l����a��E�~��g���[t|(#�[��'�u�0���+����5"�a�T�a�^qvF����n]��������_�z+^��v��Q{�Cx�t���+o��]������t����+\oB��������^8���`z�g��Z��s��)�u�z
5|E73�=[�����i0�(������$�?s��#
*h�����������UUa�Jn�;�������*�?d��2�X��c�����0�����g-�n6,XQQE%�������U
�����Ke�)�aOK����#�Y��Wi�����5i��l.�Vv�C]V�qnj�t��U1���>�}���*
��U���u�5OPh�m=D]j$�W5�8��`��x�_���2��|��V����g&��0���'�7��V�

B�\�5�9�)TQ;?p�g��,�1/~�y�|O�qEEUPn�z��"���(�+k�?|�-�V�VL|������Q����!?;�n��W��R��tR����F-�*��
�A���U���iY>�^����c�H
�^�w�#%E^����t��T�0�I�Wy��V&�g��Y�e]��A3ou��p�����EUA�����Ow���}�XU�}{��e�C� ���nk�vv��0��"�z{X���"$"�.L�����FU�U���=�;{��1
�*�
1�*{��Ot�*UQ�=}]����L�|�����x��w����J�6���^����ktuH���F-�w��EO@7m���z��7��*O��45���[����O��S�Y;t����i7sm=|�3���@� G��6��q{�!a�'3=d�������'=�;����Z�0""�u���'���"����sk��93AXU�TO}]��=��H"��*�����;����EEU�5���7������2�}���z��M���N�pbEG���jJ}��0�o��[���
���p��R�'8?�I������4��gm�w�c��0e��y'N(�,*����Yz�n�EX�vd)�Q_��r;�b"�����x�h�����=����u�t;,"7��zs)��C����#	�}�3�K��L�8��
F��h�����WZ�5���2$PQE�TV��3�x#��y���y���s2{�������+B
�S�dUL�R����z���<8���+�*����0�����������~�w������7=�]^����7�n��aF�Vg�_\������k���y��Ty�VaaUQ�G}������o�u9�W�N�r`TEAQDEG*�"^�����K����.��������=�TUET@PR�.�nN�@��t��
�J�DDDEb�F%�;�&���Oo��9��_w+FADTPQQAy��I����\�7�]�UE�wm����d��������e�����9c�WWFD�+c�����$r�>����m��{]v�z�65V��?G��[�M��R�%��n'�"�+1O�����DXQ\��3����aXQ����f���UEaa�Oj�g�h�� ��������ET_G��=4j"B�0�E����9�Q�A6cZ����~�AE�UUa��}b���J�4����7���������6U�0��0��#
����Iq���fei!I�N����;jy���fv��QTaPTaED�U�>HA#Gr'���Z��Q�4�3ws�j*��"*
��o-�X���Y����o3.��Yk-{�u����
�*$+*)^�s9/f�������u&v�V��B��
�"
�#

*���3j�����l�^�ws���e�k��s��UUTDAEQDXP���f��\�w���Z^������M���p�#(���K�|$o@o W���@��������"��("����nw�9���6���uh�I��u���Paa.�����os����j�D+��]��\���(�/>�0f��iZ���\�)��c�'�V7�)��]�}����z[�v��������2��1�;�QM��:Q��X��=����HF����So}[��*�*#������5q�B�������}Y9��T�������k� ��*�g�����9�����{������l�""��������}'+���EAEPUQ�j�����o���F��os��en��V����"C �VN�]W�����[|�s1�<��y~���^����
�0��0���q���o��+oN�m�Y�����^�/�y�U�QQQL����[�w�Gf���9ms{O��O����zXaaUF.�X\�D�������������8��EDUEHA�T}�;��E]�M�������w����2��������*��I�-��Y+��g53v}��/��dqXDQEE�9����{��p��v�N�oF^I�v,�!UTPTFTuu�>��o��f�3}��1��s��&e�0��,"0>�(P�L��va��H�*c7)����Vc4�\�U;���d
CQ.g�+L�Q�\]�={�����&n�$C��
k�Jw�>���
�mKJ��="�m�/A������ (�P��l��<* ���.��1�0���
_9�9���`AT=�g��>������}�~�����XQ~���v~��"�"04�^������"��(�R;u��*����(�**�o�����3��p�g�A��q���W��w��QU�QQDQEbvo���g]������E7E�� �xTF�Tab`E{u�����{cZ\�U�Z�d��^�[*�P�@�
#"���R�{�i�G7o�}���$���������,#B�
��U���*����P�,
3����+���;�s�� (��(W�)��B�
��vJPK��\3=n���d*�� �(e��}&����mQ�;���<��^v�PUDa`X�J�w���u�gV}�����<.�C�
�+�+�����[Y���x��������������QUQDa�T���Gf�McFv�*��/�bUB2Y���Uz���)�#����wi�bU���`�����w�s)�a��g�*��A��j��u����]�w����Os���q��G�UQ{��]fs��HQ����l
���1�Q��o�G��V-z��g� E�Qy�<��kz0�"'��b�L��BV�TDQbNW��%��V�����0�+�K_1�&2e������"��

Yv����=C7�����B�\'/�����f���("*0����W��S���o�5}+�R�Iu�pEQUQaY�l����k���{<�z����76�"����*�B*#�'�;��Y��v=^��w����}��s�o$�xTD`VXPQ8�;�}��}������qS�\�����7>RV��f�\+0�7��&�R�/���H�l�EaVaAU��\�v������v�/����s�g	�B����,0���{���s9�Y�����:��aQQD`EADT�6\�����g�=�'�������uUUFa9��V����:3������"4@����+�/Kl�������i�V'm'���C�����:������.��HQog
�ep��2��c�7�TC��x:���G��|��t��pi�<���EDS��O`�J�AUFk�{�rV�Q��o����!UA��ky|�8"��_�|e���Npy��n}3(�*�+s����f��p�AHQ����w���"��QEU��}���(��L����n��������#A�ADaQTHXTd�w&�l���u����f	h��(
���TDE�v������%s�Y7N����+�v�����*+�n��� ���w�](��f��~t*�
�", ����^g+���t+�|"�{�$H���U�}]������@U|(7������E�A%�{��Q�\�L])UED;�%�����O{;���yf[��TDTEEAQP�L�{7��q�9;8�k79y��wy����EUEaEi�!�;A)4
��AgE,���QHEHEQE.��s�3]���U�s1�Gs����KC�X��	���*Tqapj�J��K9dB��&�p���*��5���
���c'�M�m)���;E��{���6W~����<0�*"+�������l��QX6m���y%���Es���rnXJ�������
���l����}��!�PE�&Q��*\QEE���\�T��($��2�aAFEEEQ�[�@���U%u����M���^xT�XT�W�=������fvs��}2�m�KEX��ATQ�+�����G!���x9����?0e�w�����$"����*��g!,S��
M�gi�wQ	+;U(��
����ks�s��WOx������s���zo����UQX��*���Fvm8�Kn�K� U�EATEUzn�-c����x��{;��u>�����**�������=���%��u���e/����|��"�(�0����e����[=����������(�����
���U}{NHd@��&Hv��F���������j���s������t������w 9����[�^��#��U��N�o�p=�����a�B�
�m�������w�[�� �
0�{�����>��|�aEEQUf�l�s9��+�6~*�E�v�PFS�}T��W�R(UF>���O�kd��""���������jUQ�A�]N���(���[m'*���c<�EQU�ETETT��j��U���:v% x����]��QEX�Q�V�'���y�o#/}�c��s2�������<##�"
��_9��wW�w��W��f����K��N�T���#�*"��j���(�A�bQ:����39��h��QR��R���<�w2�]��0	����gUl�\U�&8Da�EW+
3�]�h����:��9�����t�������EAPXQ�W�v����t����)�zp���*���UUTEPVT}{�����MMx���v�	��=uo5��'#TUXP`Q�DP�;�L�e���
oC���$�w��Ma�����
#
7��'|�oS4k�!�V�$��h���C��cp�����)�1����Y#7-��C>���c�!y��X�<+�z�������
��5W{.�9�7\�vy�{77�u�d��4�`E�VQX����\}�6<C�"B��
����=�N�ks�Z"��0��"��"����y����MVxD`XDa!�k3�u�F��aFQEAS��|U�����QA�XaE	�;<�r�aUPXA��T������pQAa�2���N���iU>�o�|�������W��b�ZY��85��`-�j��:�X�0g)�;����Kg���y��P%z�Q�N�o'{a�i��c��Q{�s���GF�'�p��YG�M�5�M.����FMQc��F}���Bg-���������
�f("F����C����^�`����[�kq�u�n[�@�u0��Y����D#��[����[9- �gC�e�q��1�0��}G�fVLy��@d�tw����la�������W2�yM��ut]{��*%�<�l�������.vi�m�.����~��B�I���y���RqO�i���.�	��2`f�*���.V]�Z���fTX�V�D��>O^����:t�����/s�P�w�T}���K�����;`j����P��n	7~yvz�(�I��	�6^H9�uj,���;��|�%���/`6%��%@�5��L�/��P��+t2����^����G�a��}�+��
lB�}�/+
}�6�(������`��][uwiY	Y�����D\��eu����( 7*�L4�*T��k^�K���������q��J�
U6�P
�,U�������������T����v�:�����Pq���uq9����ppWM�-������W���������X0b�G������������4m�6v>���\�w\����d���}3��M���q��C��r=z2L��5C���}p=�/&I�\���Wy;4�m<Q\�Gq!�E0������uo�`�v�-��r��2�����;W
�i���2����D�U��������i�9������V����}�*�e��{V��5q��wXF.U��f���.KO��

!R���^��Qe�%<�����b��=�ks2N<6`jW<X�]��^�]m���]����UN��J���F1f��NT�!�s*h���q����=w�9�N���w}��_���l�a�(�R/���u��Y)Zn�UX�����p�rf�SR��%r�7����W,T#2���[��h�����5���3��p^V��,��k��VdlNc/���X����b�Y�������9���\	�)����J��v�.�,�������
<4Ra��;x#{r�!%N|�i�8�h�7Q��m�T�f���q.�
������^�=Tg�v�|��J���Y��Z�	/�p�H.�bv�,Do����-�M`w�`��P������F���/O���6�\���\��)��8R�2��)�9!�WD-k7��u�@S�5���+sYF*��������Jiu}��P7S�k�.��A������n���^���0wu.����]1)��@���y�C��h=���K<@k�a�/�,�l2^��a>��[0=�����p�Zwe9��]�����N�R�92�W]`������i�K��y
�{L�],���j���@b�ad���`��������9M���-�ghvv����}�O*������}t���#�b&.����Y����!Dkf�&����b��H�����&�k����Z�mK���R�^�L*�c��_nVZF��4c6]=�:k*�.8-�e��Ksn��$���*m�&���;,c�V$M��������&"��7/z^\�#J���dpV���]Zj��M��>��UC+���J,�r�KC�L���[	�]�K�u��:�*��x���������-��N��2a�!W"*�<���g
����JnVnrY�\x.�mQC�U�7��X��N��[�xVp������J��4�����%���j�z�;�YoC����6(.���(���8�m}��d��s5���NU�b]���5s�	 N����=\����%,7�����V����(,��z��}Ia-C����@t h�'�-��������F��z��h�Jk�d��d��q1LU��1��+yU&����AXn�����4������P;�������c��^K/��:�	@m-{c�����G�h�MKfqWE��7fI�gb�
����{�Y��es�(��H���bGl��K��y.��mz���N������XK�%=)\���iC)g\Yf�^.�����]��B,{h]��z���hx��*i�4����kE:�o.�Gi/+������Qz����Ag�M*��X���|����K������u����g=�������O��<��O~����-��b����{���1��</�N��({������T���xy(�������51��!�N��Q(6�B;+f�X��a7�n���xP�ou�h����]��I��q����{��x������7^lIds�U}��ns���@�]���G�j�c��h=��9%{���iP���56���Af]7�)����f�.�Z���N��qV�.��E5�)�s�cX7���s��&������i���K������,	;p���' e.�u���-�D��9���f	�2q�l1�(��(��`���0dv��v���tB���;	���{�����=�T WQIgfU��^b���h�93�}�����yY|�.c�]���W^TD����oz���j��$���{���m����b���n�b
���=�����mC.�C�������G���k��s����=R���Q�p����r��P��)'r�t���I�C;lLB��Cmv��e�1�hb�l��v�!�]�����������O�e?�]�0<�K���y'��7�R���F)h���������x��|�!�m����1
�i�ev1]��{��1�`�o3=�w���<%��������v��i�^�����r���7�������L6_w�����*�C�v��=��!n�!�m����mC����Ng�x9�����Qn����z���J���B��cI
����E���x�s������0� ��1nv�!��C�]�!��LCv�C��LB��l�x`�|������w�U�	z��;�+<����yRu|mG�w��=�������~~��C;l��m�l�b�i1���k��1�`����i^f�A_��F�u=M�fs��*������h�?�!�����
H�p0��[��j���+$L~�wG�>wl�v����I�{k�b�`�+]��]���j�����=����f_~��v�Kj���X��7W��,����*�X���~��aI|��ZRF!����;!mv�����]�Z���=�����m�����?����T�q��*f��<�m%S�iF�E�^�}�X)T���0?�Z���|�`�+���n�`����v�������[��>~��y���������1_	9hy/���2���iH�6���m�m��V��n��z/���[;���1v�!uvA��1
�i1y�C��C���U*��c������	����2������#�����nb���G�W�u�����92���WE�	]KSd�T]-c��1�EM��B*�J�vp��r��y$v�����hk�%��/�m��L�9�c�hz���3T�_w�j����e������z�������LC�}�W���>�>���_vf�|�����<���h�b;�m�`x{��R�cvbs�����c&�OZ�s����������2�)���i5r���0��{���uA�����^��������\�+�\;�	W:i�-9�!n�d�[�r���z���L���|��{���!���c�w[������e������`��8�F���v���Y���2n��T���K�����^8����a)��6�"O����gV��@1�o/.�-5��o���m��joD�X�c'�P�+������K�L9���A �~����|ba�BT�F�l���z�^o�2��{�����/k��wX�3����>������{��bk��l��!���!r��]���wWi�4�`<1��__TQ���cw�]�f�m(��R���
O)U�6�
g���j_�}��������bm����l���LC�v& ��!z���{�3<0xM���0x[��:��m���z{?XH���2������J�U^���<��}�w��O�]�1
�i�Wm��{���U���v���]������0x������	�~p��s�W��Qo,�����&���D�I���}[��|���]���1
k��=n�!��C�v��3v��-�c����;�i�n~}/������}��������i�����+���� ���M�]^��������_~��j�����[�A���=v�b���l��C���`�����	���<>O5'�3}�	Zh���79��c:��MA���Xj�^b���a���.��#���~����W;�r���n�i�sv��v��6�!{;LB��LxS���e�?X��G�TU�r�a�=�+Oi�f�����P����G��+����+v��{v���lb�c����{�����mC��LC�����^������
�����rA�������������WU�H�Ta��^s��|3v��z��1v�1m�LC��C��hb���v��v�G�����p#�W
�����r�����N���"(�b����h!����K�|���v��mv����bWi�Wm��f�!�vy���<'���/~*���%��qY��6�mtn!�w/�������i�n�/o���1��������m1n���mC.�b
�`�-�cZ�`��'0`��������$�+���4*X��S��}����U��+'^���$�}��c(���J���GB�Qxs$�p
���~��������yK
Y�x�0vr:�9���|<����XiW:�d��m��[shi��������vn}FJ��,�V'�������r�^���Vn�=�2�o������d�K��ZWr��������=�����Y������g������X��W:�uT��m����rfAwS���y&��w�td�q�����Y6���K��^�<=��U�.g� ����%��h���p�����e��H.��p��E���B���B���|���V�����x�;��H���Q��K7��D�k�
u����N�mG����r�����I}�����d�~O�w2�2�K�7����q�tc-k2$N�@�T��KuW�O���3�8G�IyQ���7���\��HYK����`�6���EV����0��]�n
����	N�l!���)n��j��Q�"��������p+���h����v�!�;LC���{m��{��b�cz���9�hb0�{�<��}M�twTB��bCIS���x�2w���3l'B��Yn�����O�����;!����{��b��b�c�m���
7���?t$��(������(�5�wV�Az���0f�� �u�)�;��n'���1n�Cj�1n��]�![���I�9����$��`�|��AW�����P�����SP:
W�����}B/�)������~w��]������v�1��1
W`�;������B��vv1�������������������1��.M���nw����
�X�����+}���>��4?�^�hb�hb���������1nv��v��!����=���}9��������g�
���X���U���_c����R"0
��Og�3���E/�U�b��R!m��.��=����l�����M����e����W�}��N~�3�����������]�!A�H�GzB�����[�����6~�[���;!��������b�`�9]��=��bv�b����fW��)����i�9�s�6�L|+;��i��/���Wn+w�t��������C����B��b
����1��bv�!�m� ��� �s�x=���n������V�����#*�t�����IG�{�3F��mg�{�����<������^]A��LC���b�hb���]��S���`�����`�����?;fo���U�P��VY�Er������k%���ST������]W���%/�}[��_*��5,����Whb;i�=]��U�g��s<0xkDkd��n}�]����U|�ta�q,#�o��b���0F�0�������g]u�O?h�y*��r���u\���F��xe"�;���}'>�X��T�����3}ga�����~�)ms��UH���|f�E/V�+X������!xMf(����%NJ������bsqo�s���w��h���L�r���a������f����5�6�A�{��hF�'vq��4�{��;�����g$�����P6�sJ;z�����<.� �����7��r�\Q[S��U�}������)�H7KH}����Q{��W����W���J�/����h��>��5�2�m������z��&lw��K���k{�0*���e�Y��(�oqV�eV�)�+wV���G��D���!T>��$^�S��;���bK�
&:xbOlxwX���\f
��&f�U�gP��x��h:cR^����h�M�N������s����)�4A�'5�os`�#�1c��!�v�!��c�� ����C�v��{�l!�v����)*�_T];w��~�#y4���2Vf���b�JP�:�68�0
�3��|��������!�;@�;�i�\���v���:��!��!����3	�����3������������tx���-�+x�o�a�35�c?+����>�2�e�yB�����*�7cH)�i�-v1uvB���w]�����.v��/gi������O�}���@�W,r�������;�w�79�K��c�O!��+7���"���k�b��1vvAWi�{�bb]��=�����1(R��}[��� �����S=5���VCW�*qu�Y��Vg�".,���_��}������Wm�]�����A�lb���m�A8N`�w������q~n��u���sX[�s�Sv��|/Ijr-�m���aV�����5v%�='���Z�T���JSy��vC;m& ���!�]��u�i�wm�1�>�>|���`y�c�q��1�r��Q�vN���o���o�����������!�v!�w]����C{m�C+�1��1z�!�vB�v����������=��v�9�����M���w���t���ms�/�v-:����%*A������!�W`��!���n��;��b.HV/���nQ�/�~�JY�M���)f�_�{�V���@$;��A*���6L������!�v�!Wm1r��Wl��v1u�!�v&!��CW���/�}K{�O\�'�nhq��B��b������w$~��{j\e�
Y�3����X�3���v�b�m1Z�&!�;�5v�!����]��z�`�/{��~��}����?��]�m����g�U�a4�Z���������xFc:(*���S��������g�j��T����wZ�T�FQ��T�y�<Fj��.fN�0W�9f3|����yR��4)�W�������Z<S�5:6OtV�������*���m*�������8�����-��T�v���w���P�LR�s-������O����������4C����w?�`��e�������x{��y���Ya�]oW�RGZU�<�h{C�7>R���Z4l�+�N��(��]`�z��t�L��Y�,��xC1I�s
�)��C�!�{C���+il��Rxq����&a������u2�\��>���[��u�>�
��o�.>�l����y:�
]nlTI����|�����`)A��N���:�����}�����s��`��KM��Sm�1aRF��`��:�
�]kP�29+k��fx�^a�B�����.�Q�����{����?e�n�����i��1
������{�l��mB��LC����������|��t��kkq��WFp�{�S��������/�u�&�sw@	���{����b����v���vC�]�bWhb�������b����������b����7��go�^j��)^��z'��r����_R%%��������m&!��LA�����1[��!]�C]���q�;�W����t�������M�S���]m�:Xe4x���*�$k{4�|�������i�5��v&!���v��z�hbW`�����;'�<w���{�$�1�^������}}u)V�����(�s�=����~3������������1z��[�1
��b���/9���3<0xkk��}��Lu���o�����T����������.\��@����{����}������Cvv1Z�LCr�LA���.��.�lC��4�n�t� ���4���'�����_�t�3�-Xu��8�'�p'}^�r����o>��������~�?�ev����z�LCk���;!�m�CU���\�b]�1�������������N@tX�����H�Y,����m%���3��e�%aN\��?�^;LA������b�hb�`�-��1�lB���;��������6����6e����fb�k�z-P	���d\�hP�������/wW,?�u���j���{m�����{�lLC�v����0xM^f��1��y+E���.�>�?e&�U�������^.�m5W�:^�Z3};���^�k��_Vf���}Gu%A�V�LC7l�r�C��Cmv��j��n��b�����}��f�E5�����R��u�z��������J�n��]��hc������5��F�g�7P:�rz�}��\�01�=����L1��M�vca���K�i`�sKP"M	�<�����^l�����b�����Y�i`V�!�����hA7���v�91���cB�>�}��X����8x��f%h���������h.��{�jMf5�l{���?j�T����s�/Vt�U�<=,gnu+��s(�.��BGq�N�k��<ml��#��k4�����	�Ct(
�)������&xb,�J�236<���JL���e���������h91�o������j��RH+B�\.����!3�u�u�� �;��p�&���\�`g_�Y�'�E�4�+td��4�^fw��Vs��f%e\�w;y5t��\L*�����=cvkO��Nn���
��:���b[G8���i�P��O��fRK{�F����0���)��A��N�hf}����v`��g#��wo��!����m]��6]����]��9���6v����`������5��O}���#���vM6��o�����N�b��x��xYV��T.������d|<	��=��j��!�v&!���b�`�2�LC��C���b���>�������O��X��G�J�w�8�����U���Nv������������}CQX�9�i�mv�����9�LC[�� �mB��������B���������F�������8�����*�,�K��3K���l#����=���/;bb.�n��]��[]�b��A��A;{��p�c���y����'?���+��Y�>W�~����$���������C����&!��!���!���������+v��33���9�`������a�F'5�y�-�+����8�P?�c�z��*�#�~�_|��x�[��o���C��CmvB��1
�c��hb�bb���!Wm1��LC��7�����~����X�6uH��+4m��`G����R��iR%�����u�?��j���!�;C{;LC���v���:�`�7;i1U��*���}�yfW?���/q-�OSW:�O��?�.L���]�r��;r��+[��>�>	;i�{��s�����=k�1
���v�b.��7m���n~�=������?S��fctE�����j���]=��= g�����x�����m&!��`�=��!�.��]��Wm�!\����J�R_*���d��v`U5��!�;t�k{+��3�@%�^�u������&Q���6���hb��bn��!���!����=�m���<yY�����;����g��v����{�Kf��m�,���X���9"��s��K����[���L���b��5C��
<�$�#n������+��1*�h��n������=�d'B�|�6�d�'@�W�u�fGr�����:��z�zT�y�y���6[;�[����0j�A�6�=�}��+����@m�[���������)>����y]��7u9	�fh�xj�����%���{����������S��^\�mTDnv�����rMpo]]��$��	�N��u.Ul2�/�W�wmd�45�ny8��7	���S����5�]��S��7`%�Z��-�sfb�}���8@g5����^�y����ow�>�	����Te�R0u���t�3g�����Ro&�u��c�m}�����<�X[�����d'TV7%R��G�����K�;�)�`��X���+�JZ�u�-���V�l|�O��P�+(�Y���&b�����8�%�Y�t�+(_x ���+zo�+������{mS��'^�9���������9VG�g���<O������|�,����e6`����b�2�D9�vnU�O/T����~��i8SNUD����a����j���d�U��VO�,�g1eI�wqX�V��2��+X�/��e3F��W� Q����mG�E&�T���5T<��<��<�V>�b��_3�J��o��s�"BY����XGs�ez�e>O$�_�����1�na��9�]������$��NJ�����=�T;��=�X=j���@���O�"q�������
X��Wcn��rG���Wo{�]�a����U"�V�=����=&� 4I��ks%E�K������.v�7.L;����_������=���o}�����z�-����X{�VuP^�'�T�����@��}~w�����$^�L��I��]�������n����HW
q?�9w[M�_
V�,�K���:���Ud����Y��I����c���;�<5���:y-�f$�*`��1
���N�a�Cn�-��]���eZ���9����������$�)�*��y[���=������K��V���<�����h���V
����8�7�����UP��z��u�%�T��V[j���������O���s���o;y���kD�xj�,�s��0���;��oeU(��Pm�J)!;�X<���mP:�O�<�T������"�@�r*9�)��7y�1�a������J~6�������]�B���+�:��[�����C�s�:�h��^�������������f4���T��
�K�9��U�#T����E�3����w�iv�/>�\�x�����Q�y\����b�k���/xd������qh�7��=����Wv�d����F-#jt�x��� �~��g�M�����=��[r,���T_m\�~���x�Q�I��<=x�n�{��M?z�Mc&�6��<�U��f`�%Y��b>������5ZU:�#d��)�Z�I
����,���O����.���]���}��I\�	G��%U5��-}}��B����J!<	��Z��`J�9m���l���;�3���k<��1yS+od��5mf5#�h��
�R���B��������ca�<����D#�u����3�8�d.[�NVs�P�:��-�"�X���&��@^'d�%C$�U��	�e���j��X����~o����Hz�+�^���m�|����b
���um�?<
��5}��I|
I/�{U'����Vu���T�-P�����,>;���#<�y����!��=��X<s1,E�������zo�g*{z��V�+%z�/����/��� ,�K���n�J5�!>��u���J4y����n�>�w�U�I�)+�b��Cz��+vj�HrW��'����Y=yRsU�`������}>2�wxN�s��G���k{'1�k��u� ��e�z���sx������'�&���V7��;�'V�����5C�U:%Y����<���
��>��&���3|O�����W�GE�c�!g_��������z��I���kT�X>���k"�a��I�|������N�3a�zG;#�;���,K�����	�.c�U��2Fz����}T0�4{��=��[Tj�sT������� f�����i~����ef��x�[����r��WI���{��`�Z'����z��U������d���O}I>��'�Wd����������D����c�(�-e����q{J�6�]����y�?�w��\��VwuP��`����P�����{���V��/�����=���'; ���z4C���{u��l�wH3i{��zT[S	���9�������U�w+=yXy��z�U�%�)7{��j��C4����<�mGcK�I��]{��;6�p�I~�w�I��c�L����-��8��kZ���wI(6��S�G.\]�7��i��� �'*�Ip�xA�~�f:�GoT��IR����du>�?j}�?nm���/�L��������kpPb<���:vi�XpN��=�V6��?��k�
f�}��|�����Ki��}	���T�34A����{����#r�;�xW.��[��v��x���3A�,8]�����k��ngu���M,��������+3��i�P�r�q��x������y|�����.gc�����@	�J;���������YhoB�N��u�e�ce�|���@�7��'������x�������|2�7@���,�(����:4R��`���a�������$)�u\�
�h�`�V�m�e?G�Ur��Y��0�Py��q�������y�i�BV�����j�������r��y�9�xw�������P��H�T��;��j��RO�NJ�����wj���R�
�����L'����n}w��^i ��~�Uj��������Y;������Xy�,�1I>�����4�����2/A!�q{�:��#3q��?\�;�Gn��������n���\��N���sI��3���U���{�Vz����B�SU��J|U�����S{[.�Zp\^������3'�		3
���6�������IkVw5e�����d��;�Xr�A��O�>o�?����������^z��;��oap��IOT�312�h5F��)u7�0�������T=�V{���Z�U����a~��b�^,�W�����n���Wnh���peb��J�8���*Z������Z�{�U'{���a^��������'7��x��������{�i=��g/(��@�t��Ck�f�����������7
����U�uT�UV������z�-�'������w��o�h+St����'}�2���<�;�e��VB��?}cI?}��5��`�U��U'<��������k*���W���p��-��_$u���Ge�����]{�|s���\��[���y�T
�I}���RyZ�RI*�&����>�n��v�	�y
�o�V3�l��U_K�@���\z8���>���������mP:�@�j���`yz�U�Kz�;��>��I���z��~�A,G�M\Z���9}�5��8q^�<���vm�j0�l�J��EBX%Xu����L�mA"�����c3�/���C���r�-���A8m�EA���%�����x��y��t�w+77>�t�x8�]�����\K���w!��g0L�b�X5��gYb8���,S��=�������Mk�x#G@:h�'�g�U}�J>��k�������o��IT-�C�p�9u�t���{�M���^WA����6��e�~�ly.��G7�L{��m���9������`:t����B^u���{BU��9�d����B(2�qGi���w}�s1*.�45����:�]�$�I�`�U:�����F��
��S:��I&���4�����T����r������zed�v��a7g��*B�����f��m|e+�]+��j�i��/�#u����T��]\�������c�u�������w*|3l��[���-S���4h���y�q��S���v�������C�y��>��z�=���T��+;�$�
�&nj��MY L`m���V5�W������%�����(�����/a��������������yz��j��j���^VO=����+Vw�Y{~���E����T���Y��PCP��=4��>2��]\��N����������Y��R��U%���RV�~�k���+7������k�l�+$_��e7n��!da�
�H{���T�'��d;�Y=��_I$z��!��T�&J����)�G:��}D��7M�3�>�d�<�+�n�R}K���>?�To*y�+-����X+T��j���V=�C�������o�N��K��t���hs���5^����������_k�����V��+#�Tm��Y=mRs�d^VO^�_?��0��~f���w��o��Zi'vZ��^-*��d�=t�]�����u`�T����Z���[���+=rI F^���Ldd�KgvRk/6L\���5mz\���?�v<�0���������V
j���-����cVI��>6I i<��/tK���G��ox
�ox%1���S��Q�xb��a�y�/��'%����TTVy��/5C�����X6��d���8g������)���^:������i��`!v&3�
�������iI>.*��>������j��9T�� x@�B�1AY&SY��X�b�Np0�g��.����V�f�X�BTm*�2f���I
P���mm��5lbmQ����6���E����&�@m�a*����36�YU��cV�4KT���a4�J��Z�*��Z���$��J���4i�j�j��Ucmf���+YQ�������-�mL�AY�"k4j��f��e��6��
V�kf����)6MY"���K0E���d��Cf,��km�1IBZm�-$Z�l�ZPP��-kT��Y�2�*
CZ�0�ZP���m�eT��"�6�l&��4Ca��EFMZ��m��f�[U�5���B��2V��%Y)l��eaQ!Zi�M���i��f����X�05��*���RY��cQ6��Uam
��)����� f��2����
�#`�,�24��a![4��KR��[[l��m�����&��f��Ym�eH,���
�
e[K"m��-�4Jf�M-d������J��
*Y-�f�2�T�3Yi��V�P�J�if�f�F&j�Y�ZVT���`�(�R�6�+kke�m�M��Kf��
�2&������V��#l�m+M�m��CV+SX��kYA����SM���jY���*������0�R���m�Ui)f����6k`�M�1�Z3m����*��m�6�V���2�S-�V�m�a�$����M���m�i S6�ZT4�)�m6���M��m,��5km�f�����l���Um�i1�R�cJh5[I2����m�Vd����Ekjd�����M�[5�h�M��cV�c�m��F�Y����f����kCj���l�������iU�Jk`��f��a���dfT��$F��e�����*1��d
+eUL��f�L����m[T�Q�1�5���&l�����L�H���RT5#J���[3���[m���[6�Q�����������Z�if���h��,6�1U�h�Ye�����jY�-�F���j��X�VmB�������efB��Ve���h����4������[Z�eUc6m���j��bYU�lAl��5b�E*el�l���V�2�2���3Mdm�S*�X������[Z������-Kl35k2��$jQ�*���5�d���$���+R�m���d�V3&�
6k2���0����2���CR��M�k$-#miU�����[I,cL�]�;n�����:s���,w�n���v���tw,�k3j��kt���Nf������u�Z�:5�����;5R�������n���w��;�wP��J�R�T��D�T��Q*��2W��JM��ke��$*Y���i%�m����K�u���R�[Im�R@�����m��JK
�H�6�m��m��M!A#m���K&Y6��T(�ZV�%l��Se)�UW2��ke-�m��%Ekm������M�((�2����[[7^u���n<����������=��z^���ya�/u�9��o8v�u��d��kmM5����ev�]�s�tUr�7"�:U1�zzz�m��M�'T��z[]1���%D��v
@�D
��J�J���8���� ��
�P�@���o|���`����@lw` (r�En��v;���@��v
����{`���m�l]�����
�9�S�9w��{2]�f�����sgl��r]��V�4�%w2[2I)2iv�����r[$������i$�wgl����-����V�d����.��]�������]�w��Y�����;���=��G�n861���^q���f�p���������m[{8���Y4���V������Y��Ut�W�^{�(���6UAJA%H��$*T�#�g5����[;f��l���������7kZ��;f���
,Z��������%'&�%N�9�N�����m�m���(�����v]��l���pN�r�k)kwwI����4�U�e�L����.��r�.��4���%�J]��F,�svn���;�vm����h���&������[�9-<O,zw���������,�7R�]��u�^�=�y�������{�Kv��P�w�v�j��t+��Z����R��[�����d��z����R�pk��z��`AJ�$	R�UJ�$�IO5A�I���mi;gl�$��iTN�m�;gl�%����������kEl�l���eT��IvSl��+f:�v����N����6�\C�9��rn�I%�l��P�Z��3I;f�;f���*
����kY�R��[$�P]I��m��vi�6��r�]��r�]���V�����z<���YX�y�:�������������'9���SMi����KX�s������{X�{z.��ow�]��i����R�V+��<�&����:u�l��{��P$��"�R�U)��S�����v[-�k�l��rN*��)sM��-��v��j�T[���(l���YC3�iZR��t�����m��.�����m�kbMwm)v�����nM��RWK�n�m�m�Q�Dkc;��]-��J���:*m���P�e��YRSC�mv�)Z�[���;��c�=w/w���y=�oj��9�w8�{���VX:��r����U�ky��N�Wu���OM�y�.���w[z�)��YZ�6�*T�V�X�HR� *HU��A=i
�Y��Fn�����)��fe�e����K�{�,0�y*�����m�m���Bi���iJR��;eB�Qv�m-�:�V���SHv�]���wGV����r�����(*kF�]�m�v
�km����N��������m����J�eiJ]������|�o06�����lM���u����a����,�z7����g����.�u��5�n����]p���������[����x��l�����m�u���z��Z���{hS]��B	HE�0$P��U	(0i[Z)���%v��n�����R�v��]��kn
[D��r��l�����;(�v�g,��v�K�n�+���v�i4�v��@t�7[m���u���l�b�n��v����vs������vv����n�7km�4X�����e�%��n���s��%�u��we��nm�>�.�����������g��������+���U�n��w]�^�N������i�[)�m7A���C����W](�i��\�]�FJ����w�=n���g+7J�wk5��A%*$�B�$(��EJJ����G��e:[v�������V�6��v]�4���QWldsv�r��v���l��8B`S�n����7+e�v�N�b#v[f\�����l��6]�]����J]�������n��Jm�m�m�H�d��gmm�J�ml��k1����v�����]��t��2�wm�������+gg�z���gN���7�9�i�l�g\��\{���z���u���
�mKsv�����wr�5J����)T��q��+��sN��zi�W�l��m������:��������D/X�P�m�DP�[l�2������[-��e���
&�������]��.�Z�b�u��kJ��r�v�]��\�,��4���0��n����N]��mm��Wv���K+l�[F�5-�mm��m�-����t(���rm���wl
���l�wn�v��m�MdV�.N��r���k�s��;|��z����q��ypl���n�������=�u5�B��t���kvv�\��N����{�e{m������+����:���M��r���5�[�s�u�:���Y�TI*��*E�d���$H��&Q[���f��Z���wR�tkWr�NR��v��� ��72����������A2�����+��+k�n��N]�f����wn]���
�,����vt������5��Z�D��m���N]�����T'9�r��6�E![���������������D���������Zy�S
�)�1�$����6�UM���
S��))4�R% �MF�T������������������Z�����p��^���	2�y
�����F�	# ����i��7m���v�����W}	5�7_vm��xM7���}����;����]�C����J{O`�_N��+��/I�4gDw���W�
_m��3�"T'S��������������*9F���[\��&E"L�'��L����+�:���yf����#��"�����3�U�W�����������wt�O-x�S�5qn�w�k
��������7�]2,��	F�[�XM�Z���T��,	rY������������&��A����WN�<��p���7��
u:���Y}�	�,%v�Y������B��Z���erNVv�k�-�����^���	�'>������W�Pb�`�ng�s0xq��e���!T��DD��zCT���(�<��x��bC^Z��:��~;"u��[p�j��N��T�w��3�6R�����N��R�����9M�H�4��7�O�Qr�ZQ���&��l������	����WT�������s��1iG*=�
B��m�u%\�~i�b����/�b?�|Q��O��Y=;+o��l�Q������nC�p�Ml\x���v!�UtDe���
�gNdjDs;�J�C�w/TH��h����[����1\���r��\�{���m)���Y��:*�g$3sh_>��9p"��2	W�p�1^p��m�W
�S�d{���������6J��'wz�
���4����C�*�����%��py	$=������8���o\��������L&8�.���b����2�����+s~��>���#���.�|\�k57�^h�v�A��cv�`�'�=F���`���h���T��7�0��aF�q�g]3KY��c�\T1.�V=�������[=}C����g�m��\^�^E����,l'0����
	P����d�;�(|���nx��R�xa��C��J���"��U�����	5���b�����������GI�:JJF1n����Y6���AI���\n-[oo�_,}k�;��8��W��}�<~������rf���Pf����72�c��DN.j���� �OX8)�������xj�=�r���������B��R*�m,�D d\1
	�x]�r�CS��E;�����w���KX��#�$���S����c(���������'���#�I���
�����v���`���}�r�~�����F��z��6������������g����xs��T-��3
��>xX+�ncW��^#�����${*��v��K<k��<^;�
�s������M�*h��5��yR�Vc]��K�.������;�%�"���4/�n��N���V�X�v079�Z�}|j*+�9P���m:����������g�����
������W%���7T��;l�B���]�u!5��V��t
������:�K�M+{�b�m�U�}l��I/r�&�S����C�V����*�$
9�����\�z�q��}�zZ��fB-��]�W��tZ3��V�g?R�Y��4K��Kf.� 7�{6Z���M���~*���2�b��'���-_]�*��W*\)�J�]p������S�������*Sol��f<�u/t���\��s7�]�S7���Nb��}O5�,��Jg�D��v���w������T������W:�+�R�A�V�y��pZ�gnX,-�;5�O]��S�wU�wB\.���[~�1&0�k��Y���>X��^�A����cU��w����f�f?r'�VH6	�M9}��n�o���lGm�cJ��p��*�jU����sR-u�n�/&��Y�{Q�6�P6�7�:���n�
��w���icd�+Ac%Gd�w;��`�SF�#h	�{y�#
�����b�p46K�����������V�������U7ga����	��N��yz�+vjF9UR��&��Aj�������fk�Q�~�1a�������f&�;6.S�����D����8�;�&U�+t��<;�5k.�~���N�.�������t%Y2S`�7���hh���������
/�gb,h�Q1�<��/�����,�*^�'.IpA�/��_�e���*]j�r!g>s����
}8�8�"s�l�m�8��w�t^�\��s�w�7����+g
���/�Y����s}
*S:��`<�����o�������W��h���L\�$O�t���!����������+�j9����p�w�V^j��4�ruS�S�Cl�,�"���W��]o5
�M�c���k6��=g<��3F	[y�;�}�=:�|.���k<���v�rmv3��w/��������x��k�%����Kc^���i�~i�O�V551`�5JY[���Z��i3,�5�����r���Iw����W'����A���t�4<"�y��P�kK��������w�<}�O���n�����[�����c���+W��~�k�5�8��{|������[�n�jZzU"h,�j�	��M��Q��(���_�������������v=�s�i�p1���MA�����{},[�f�:h��7O5"d����QG��;E��	2�OE�D���<��JmI�h���]#�n�s"�0I��T�aS%\���`�i^��iW+�)*c�.����������L���Rua��|��k�W��:zaB����oo�,���Q{]�e�����}�[�������(�o���$�}{����������5�J�k;-;Z���)��t$m`�o(n��
3Mn�S�����w��2r����\sw;��7}��IU�N]���G|���}z�=�+���uX���X��h5n���gW���<�i.�CW���b[�#<f�\���2��;��s-�����bi�5���{���7�A���Z(�����{t���y��w����(|���|9�7�m�!K��S��3J�U����M��uG�i�y�p��vk*���DY���+'�^�z�[��L�:B�����y���
�����8�aR���Ec/�N�������0V)������p�=����!NS����!���Sd����3��/H�R���� w�71}����D\�s���f:4����Ju���;L�;����K�8.�#5�+��2��Q��C|���U���w	&'����=g�yG�W^Ni����5����>�L��!#���E����H�W'B��7�XVk6�^�g*z�Y�����������!<qu0bv�����N����|���Q��_9�������^�
�:6;f%&���z(���m�mB��\��^�b�Z��Q���g4D���1�}]�UX���� �a���R"��%F��d�C�uf�����_�E�k8@�N���S/���V^$��f�����|�����v G�������.���V�7������:���M���c�]������$F&oFN��;2�q{'"��r��P��t��������W�g�8��ELG��6h�=����p�L�K=Ps2j�j"��{3����v��x?f�ag|��+�����&�%b��U��+�xj�C�z+E�\��J��
�a���.�jc���e��y��z>�h���WZ��}M�4�Z�u^�Jlb����w~}r���.R���,��z���/3~�B*�MAf��r�M�kJ�������W	�0�2f��mfY'c8u8�=���2�`2^5��1���w���QR��N��F�6�.���������������CGB�nZ%���)�o&N�=m�f+��]�����[#K����M�VV��P�#
����e)�����6��B w�]uOpyfj���q�\*��g_i�PzJ[wOw�^���9�Z'h���Q8�D�W��I�^4�q����2�������UB��En�����b�k��z-����������Igu�Z��]�����=�����T����,�#0�T|��>���%����/OS�3EV�����OK��EC�u�;��w-���Gio7�Y�����R�R��A���O%�����Rh��9vy����lWB���$���A�
��'y����m����������� �b:��U�b�~��b(t*���\W]Q���)Y��jQ��tp����%G��r�y�����+%������Fs@iJb��A���������c������h9AP���Xvv���~���� �TC��N5m�������;����"O%6����B]���������m{W���?������S��E�����1���;]	�@�>e����r����d����up�w� ��k,�>:@�=��;>�����&�~A�5�Q`�K5h��r��t�Z�iG��4�<���j��cT�#����t/��d4�X�}�B}Y�sL=��s}Lf����ic�����;�a�:g<�5d�lO�NS(�1�_m��w�g����}���@�O�+�A���B����I��^x!���v]�<w
�Y��C�z�la�m���Z�!�N��u����X�����&fF�'V�*&-b���p�	���#����M!��3�+��mh�� {���VMnu�b��}(��/�t�r�wd������ur+�G��wG����b�9|�*r�a�)P��s��qy����$I��}C8�#jb������Zt���|��7��7��:UR�p��f�QT����J�� �O!B���L�(��L�����`{�K�h�r�uy�C��b�������wI9;K�Dq����kw]�	���~|}�u����&�i:�VQ��K�tt�������H����.8�znk������a^0��_?����/m��%_j	�{����d>�Wr�d�YZ��d�������[}�������w�K|�����[C5��v�6j�PWx�J�O��]>K��!I��.���H�4-t��5p����EYM*Y��u1N���{+.�Yu+f��)�25����D���G)_@�h*6���N�����h�.v���S�3Q ��:��8_��z���{ua����.��J�h�VZ��G���Q�W���Q
�����/ nG��uY�:@��7���&���tV��i�QuM��;���f��xk��2��&_����������[V�j�!W#��������[zpwd{���3dN�mg��R3*9�^I�Wd#{o�[ �FcP���l&di+�������8���Th;��� v��NN�1q�)3��[��0�>�]�u�����T)S7}9b����(SY��<f���c�7��W���|��[��g��GS�9,7�j]}��}).���TS�IMhlw6��:T�,���P�#���j
�l�f|�m�w��6
iCYt+�B�����n��zq6
��&��]�������P���{{�Q�R��Q��L�nk��9r��=��^���Z������a7k��_?��K�B�z^�����dFJ���{�(3��[�������A/���.�����w3��
�~|o���v<�b�����#������
�������!�pV���L/%��w�d&��p�{-_	�N�_�a[���B%�ee���Jk~�a	*������	j�	����uh���[��^5�ju�.++%���$�u���yv1�OK9�������|k}�b�������KGK3���^v��>W�	z���
9�������s��<����d�B,<����)�*Q)uB4���U��P����#���`�l��UR�3:Av����E����|�y�F;4o��q�)����h�-���W}e1\�yZ�}f�v���8c��tlG�v�i��u��u[�=��8�#nelFj�q������4�k>��:�bz�2��c���0����*MC��Z�/�{v�c)��C��X��-]&z���yUgs��vS���**I����x;�%����&*2�bYVE\ ���:Cg���od�.N�`=yn>�T�0���H��v�4y����X�r��)�M�/Z������E��"�M�p*J�p�0���Z[4P�>58P�y�5T��1����[��z�5-���tq�	�#o�j=�U����
����v+������w|#%�-����f��{M1�&��v���>��a[f�nY+�L����������UN��ux'�i�5��5n]�$&�#p��00����K�B�����o�6x�u*�	�H0�E�S.���C:i(J:^�C��S�6�AS��}��'5�4�^D��</�q���.���Y�A���|Lf���*[sf�]�*c�DC���N�f��I�SnwE�_��Y<=(����2�joh�e�eh|��x�D��F��_:&z<����|�?Yk��k{N��#!�7suq^�i%�Z�}��������^�����x|��Qi��lVY2MZ
����0^JZ*�����U�M�T�������{
�G��Q�s�vx�t�V�
c�������n�1�r�^�:���8 Z�z	��m�eE4�k�����D�����=�X�f�����E=�
$v�8�{E���o�N������ ���i��wz������y=�7�s�\h��'��FqetX����Df��+��������'P��9|�0�HF�'�h���Ox��;��n��U�c�r[:wB��������x�g9Kj��J���X����K
A[�)����K���b���hv�*�Q��{�K[Y���6Ve=���B����W9�AK������#�^��q���z�w�d��V�a�b��G������;��f�
dMI��5��A�u�\�`�=����cm���N��������h��fn[��n�NP�n�d�T�����h�0n;���,����-+���Dl
�P*�p�z7�Cx'�t�l##[;DWn����u��U�b���9R?Jo�+���}w�Dn��S�B���F����/�1.��(o���b�d��>�m6�����S;���}��h�G/�a��\|b�#�Z�h�4n�E����_'���V�{N����:'�4rT�2�c���
�������Z6�7orU�}�r�|#���-�&��Ed�96�7V=`�5F������6o��ns0�J����������5��$'7�=Y��l+yK�,l���f>�R�N��U]=5���.�T8*j��WMsz�f���Q�`5�W�Wm��P��q{�Mvi�r
����e��Z�{�u�VE{]��q��$��2>����aT��;T��ZI�[�w�!O#w�;Z�v�b�"�� �awC���^�x���9@fR�E<�c�tr�p�.���R�N�t�]�d�Gng�yw�(�)��r�|kyN��U��f���%"~��Z(,D,���Q�Vl��:���W�b�����v|��O��ai������jn�t�y�]q�	��o^����98�<�������T�2�!
���F����{[#��#OqD�4!-���@n�.=X�qrEY�p���/�����$���t��=���R)GR�e�7�������{��j��X���7�������O���u����y��j3Wg&��&�9E��^�>
���C����8_�y�z��{ {;��S3_<L������xi.�.����f�s��n������n�����!�\�t����HK�#��'$��#���D���~ay/u�M��n�u���VJ:��^P�l$Md����[�����~���x����+>)w��?&������Xg�4���g�����`3Xj��������Hr�o�
����\��F�Tmq]�������l>����#=�e;��g�
	���1e����
���E����ai[&��f����Z����bx�"�t:DN���V�|U�UP�iEWu�]�|��X�y�g�N���\l���O\|0m�N��k/N=T7�,�����;)F4�oA��	d����!�|9�_��5���v��%�
1����2��y��7���^e��NyG�������w�3s��&�"�b�D�LJ��<���7Uo��SP��<��i� :i�`�����c������s3���|Mf�_O!WAw���mo,����� k[��o�k�8b|�1��WK�7����zI�m7-or{FQC;���al��gVZ����8 �5����N�~k�{�G�����
w�U���I���I�-��Q�
<�K>�G��e��u��3Y�7@�RUzvQ5������E;a�wR����]�*������(gw�z�>{�/S��^k]Sw/3u��c�	���n������u���T���J�&�r\����1c�;�p��Z]��'y�Q�6�����'Z:eu�f�<���.��rq�IXe�6�%�_�|��"teU��4J�����Fp�1j��wBR��&�+��h�En��)fc�����r%�H#����@�wK�m>������#��JVA��XYX�I>�������v�"�3 �>����"��I���UoqO�������F�v��������h����Y�5��.��V>�������u�;`^���E17b���v+�;9�'<T��(��l�����g0�����c���)]�Olz��>�z����p��"���5�W��������A��q6kx����A��u�(�^�|��o)/X��|�������d����t���W�����]�Tf�^f��e��F�,���E+���c{X�fI�b��{l�����xH��������^�t�Tq[����;�4mv���-KVU����(e�y�m�{~�������=�D#0�#3t^����b���=��ev�b�h�������c�P�;��$�Q7��z,�!�i<�\�����;p�7��7'��11�w�;�<�`wF��=�n�8kN>�e�Ft��*�z�k'DoF���J�@^i����u��D��Kj*����0
C��Y�� T��^�����S�G�{pHhL�e��w����x����0�'���n��^�]�H��g[��^a�Y"02[Hvl��q��A�'U��/9m&�Waz�V���]e�<�Qx����5#��l'�v+���]�����rySpr��MwI���^t{�5�ix����5�+�`�F�N�cD��J.�6�.���H���R�o,������I��WF^`���R�U�]�n�e.�Z�D�=:��r�g��b8�R��nb���$:����N�.��BB���V���+�.�t=��|������wQ,hB����)�!:}����|���c�@�qI�5�y��yyf���X��jE���I�[�0�������������&�gjq�!uu�mS�z�m)��N����u�x%H���v�Dv�j�������c�I*�F���_`��w��9%S�/�v�{<"k�`��/>dX��:��5e�]r�,����1^�mh�	��^��sbv��\q5K��t\���&��0�V�D�i^�*'so�r�Kw�����Z�[9����-�a�}0(�X�+\�>K��o��[���2�~�W���[-^���/(��=n)A�L6���{V�����;��de�T����{��K����e,g&��-Tt�WbRl����6CD��j����G�7�Uj�����cL;�U�������3�r\�j{��OI����/������\
#P-���d"T��^��Wv�"Sc�
��#�.���8I7HBx
���M�}��������F������|*�&=���Gl,]Z�9&��{��s�6R&y(
`,]X�K���o-���g�=u��n�l�����0{#�:l�}'�r��\�YB���[���Z�d���I��}�*��M
����B���u����h�3�:V&wX39�[�	��wU�{���T�����7������U�c<�����e+��/���6����$/�0�����e��)�|9��#�?8�*	��,{K�	�������������~�b���K��H7�'�������\���N������G��S^>�:eY�p�u��u��FK�����a�19>d�������O�]�����H`VpI��#�3/M�DG�|�
�l������^~��L���M[Tr�y�l����-�9�sV���y�#�@�mk�G��~��0,�j�{q��y&z�,��rm�,�o��1��H<_��-��������s����|g0�]����y�7Wfu���O7���t99��kM��K�n:����o��`����N,�)-�V$x�k���mV���T��p�#���@]Z�����y\BY������fP���d#J#v\q�a)JA�o���L3�V�d��{�u�� ^���y�2�����z,����`z��=������^|����v�&������faK��i�t��}�wp��X��}�E�kY���p���@�V��-�^����t����jj9��s�������#�X�����PL��kUOs^�7w�B*\�s�;�,�����M��<*�
��B��u�^�a�=&�������r��VL�f>�y�y�^��3s���������vK�.���i�@����>�]����CVl��-�������R��C����]l��1s�������Z�O,�:�v�k�R�����Z�[O��9�[������Pl(�q
5p�y\f�%�}�r��z�le�nf�;}�v(�S�[��.��*������6@�['��2�^���n;��}���^Di���:RqGN���3}�iJ��I��<��yX���|��[���Y�5�>#=���9])�'�����Bf�zli-j�E��;�v=���d�����g~���^�ZI�?We?�]���i�_���t��Z�����x���w�b~d����N{�Z/{��	����Xo0��+"�u7��.�3f��s�&����~|�gv�j�!����NSv#�<�sC{�Y��^���n�b��\��>gT��k��8�����w[l�Q�����R��;�������$�3�%��c��m�;oc�jf��Bb���f��^qZ��1��s��iUI#�b���&iv
�����"�[><�{������kk>
7���t=�����L!�WE�B�mX5z,���"d��H��/����8��0�E����##{P�T�}��6�%�oH��>H��'s5�$y��N��j�+����gn�{��-��[���6z���3i1'��`���9�G�����s�i��qZJ�HFM��<�xi�R����Lx���;���S��Kj�N&��]
�����s2$��h���]MJ������oz]U�%$�����r:�\��b����gp����|���B{�oR����V�rr�i�v���g����Sa�%���������2�Q�;Id��
fv��=��
�����]�5c�����j�x���V�i���4nu���k��K�8��XPJ�oOE����U��\�����;����s�?Y�F��r��m����}/{y�*�������Q���@���y���3`��7�9^��yqs��� �|�e�EuOfn�"k7�O��0�	�pt�Z��/������D�����f�:gB���b0�Lo�z�69n�k��p�[r��{��;[�,*�������{6����O�EU������p�:���gK���q<��3B��Oe8�R�eVa�����wPya�r��$���v�����]f�r��e;�y�+=���%�y`�X����p[�g3{���[� ����7��z�A���a6��~������/e/����Jt�e�A����(��F]r�����6��j���n�_JNJ%1Y��%9��E�*�j�2�n�n�a&�����$����l��S`�CB�<��T�����*gp�9����(�����4�}39���o���B�*_A\�l)&}M]J����Z6�Hb^/e_����x��Bu�����:����$�*��7�'�����PW�:<�������$w�	��v������1��T0m??	�7^k��wL+D%\V��Kq�����s^�����LH�����DE�/-���*%�]���'yN���:(uV�G���=�YA*n����Q�����M#�������M�I�7{�n�r�m"9������G�Q]A���"�[bM=��+�_���T�����4��-It:�6N��Ll�:�L���&xtXV*����F4��D�r@;�Js�����L�fX�:H]b�������h=�S]��
U0(<w����g*:��V�-�vO���vf��}���&nU�
o�������.v�m�����.Q�E���uK7�����U��N���N/��?�����e�.��|&�t�����(�����3�3�;��������v�5�����L����N��������"�F�$4���v�P�;gw�����yC�Wc^�
�ha�U��G��2�7��7�O#t1fnX����H�x���$��DS�4sn��l���r����Y��y{r�i�\��]�����\|����]��>4(A���d����K&Wv^���7|�Y���.�3
"�0]���!����O�
{Y��3{\�;-5����9����x�;�o���94�]�_�7�����bC��t,��#���BP����l!xv����H��61�(S=a+
,����j�\l�8Em�,�s���� {���%
-�{{�����^�Inx���oP��\���\���a���+	N��"�J�@u<��g���������'��;"�!I�fpBG����:X�K�7���������A3�t���p!��n�m�mf�!r���!�^�����'}��i��q��|`��gqW1��w��v�L3�|��2kkf-�Y�(-M�4��/���v	��{n��]�����������<R��N�rz�b�f�Knsub,��3���eD�$]��n�Uy(Z��[��+��GL�������f8��5{c�USy
������Vc�<��
�]�t����@���i������ri����V�����YgQU_0�Q2���t�+~O�
�e��Y#SZZ�nl�BtP�R��I����2���;�@�����9t{M�,�m������7LD�GS#�"(�RQ�#�9�����������o��a�9;��||r����e�CW"
�#rU-r{�EX�������������z����Gvt�BC�������z��v>�������i��=�����7D-����]�u.������fn\�F��dk�tT�������G9������|��&��}o=:b����F<e�!�0�x���;������QC|���,������q��������|�2���������q�w��*���bQ��TOH���g6��}	��[��M����g��y[FVZ������b]�����o�+.;6���P@�����s51��z������}��u�<���L�k��_P���V{�B~������ ���w'���Niq@��������E��+����c�J�����D��%�3�F��;S;5k�������� �G����[W[�S��s�^;����������c��Rv��a;��#����+�v\d������H�5��������/����\��~���,
S�S��F5�fFJ�_�y	�|F���}��}�+~��������$�k���K�:��E_K��T{\����X�L}B�N���w.�\#+~(v��9����?e-T�Ce��$�
Y}�__����Q�
����u!�uW��yO��=$3��v_==�[��W�S9{1�m�������b6�.�u�����H77�����!��T'\������~����=���u�E��.|7���e��;]<�]�D
�	t��REN��Us+z��V�2F��d����fL+��F9�O&���8��W=�~����dY�C�d��w/S�G��7�GZ�Y�Qlg��w�K����3lHf:���}C����:�:������*G|)w���M
�#�p8i������X|�y�9m��t������Z�S�m*RnM����6}o��E��/P9Gz��	1T�{)`x��kA]c����6�F-v����b��"0��`���wtAU���RY|^�������G�����^Mk-M���R��/��/9���.�i�h��� �e��0�����9� izi6]f�����r���a��h�&3M�Xd��n"�+QK+�P��wI(U�:]=�i�����"*Sy&��u�e)�I ����}P�o���l:/z���;��c��5Bgr���]�A$����J��Q$�������<���|�����:����^�.���ZB��Y�T�����������`��]�K��60���g'���{��7��{�����@�H�y"S)t��+o������v�y76�Pg���
W�8�;r��E�	�	]lvT��r���Ev���C�U��3��z�S]�R���������a���]��o����I���D[��X�5�����7�n���[W�\f��a��T�w5�*�3q1�g�}�q��I�Jg{z��H���U689�j�N�/�FI�W�}�����f�����0��u|j�������"��'oLpw���e&m�T�����������5�h��v�{zjf��DR�9\����8%u@�szTV,0��LJ���+3���j�i�,7����C���*���zu��`�/b�z�j�C�������`�'HR�P���*����g�7�|}6�w��A�*]�
4/��,�kQ�O8�s��Im�f�
��G��[/4���om�=��!:&��[Y4�tw65[|i����Px9�����u�K��62��l�������c�IF��o���F���u�Eb�3L��;��*Qe���	�������
t�:�����6a�N�U������~�,��������C=������[�oM��n8�C��8n��q��'U��m�X��^\2ev�2Q�O&��Sz���h�{Wus�������}��yRq�P��v��a@��t
x9^����r4em��m����ek������L���.c��1��o���$�jbv8����
��������2l�U�?/o�3�L����u}��������_�s
�E�|���uHh�y�����)i�������k��YX�m��C���5��}��	K'�4�H�zUmcs*�	N^3U}�4>��:�&�*�fS��i���>&�/��6t�*���[��*mgl�tR�'�e�}p-��"d���w�{[�f6EitN�u�=�y����.e��w�DQ�'��������v]�u�)�����76��i�z&��-�T�b�a����}]K������������17O���F����O��+����E�o%��J�����6����L��V��&���E��q��8�]h4=,���u7{���OI��U�����s&�|�cv�v����rTa�]�WC����������)S��<nO,��%���h����m��G��Bp/(��i�������w��e� �����(+�]XK�1s�tS+Iq�l�K�]�'�Iuf��2�+uc��
���Z������g��BNw��~x�hX�<j����1�"�t������b��|b��;��[����sf���PIi���QlUYz��_B��@�;P����E���n�S��'[~��w#�.��^h��9��Z������=f�;ts]��ZMKY���[��2�2�^N=~h���R�i�YS��N�����������������yz	��+��O_2��g=���h���q�d�.M�rxY�UJ�
f%�fS
���������b�Bj�2��D9�v�3N�������Zs ���am�:����u�Yd�;�u?�����[�F�*�?cx���?�+�����pw-�W&J>J���W9��v������[�8����
P\�7�Wb����U)^)������}L�3�����L��h���/,�+(w��y�;tLWC�<����[�;�%S�����B����V�F`xhf�1���q�;f�z������7@�VS������>i���1��u��!e���N��<���%O7�{�vd�W&<��J!R�����*?u����U�Ftn�����9�m�����Nu�:�r���/+��1��`��x�K��B,1)h�;8b�L��G��%^sZ��y��dyX�>�Z�������
������� [yX���P2Kc},�w�\�������r/r��2�v`��[���z�����i�jyS�o5��+2��k3����Y�.�P��Om�����������n���0����]�Nu���A�=�o<�\��s5]�ve���	�6J��u3��"8	W!��V������<���N"j����Mq�:�c���N��#qt~�k�[5V��f��J�[���7,�\�Yy
f�
���U�J��.��33�Q]G��� w%��i�����[-)Q�����'[i��<9���<��E:0��H���N.���_U�0+�qr�w�8���s���gC������f����v��.�����n�,i�����.h��p������=�u{'Q>����,��=2q��2��O�
�"���v5������U��f+a���rW*�w�%@��e�qj8o%���	��?ly�&�Ea�o'������K4f��� F��a
��a��T�������7�>l�I�����Z���M&J�:Q�j����{�:
��#^
��^��{3�e�+��g���#�s��:��O�p��V_���VB���e�-����������}��Kl����%������.�����0��uW��7{���M�"�F�h=
�o��+����o�B�����G
��P��Za��n�����W��Urk.��2�J:���a[�����on���W�9����]�X��eA��c��s��]��r?����V���l��W���Vx�����u�����N�n\���H�
�z��'b�^�nDwR���yd!��s�]h���. ��|��+y.�t�;�:��9a�::����nW���i�O����a�sq���q�2���s!�l�wq��+*�6D\}wsF����Nza����dm!�f=�Sx�B@��7`��4;W|�<LV��@]�����T���
�
l"���i#2�7=�\��I
eTC>����|^rk3�J�Y��=�/�q�J��]���H���.
����t�5a���0�u�V�i�KY��<��Nd��ys�J"]q"����f<3M����S�{�����K��Ix/N3l5�7�B���xX��x}����:g���h�|�c�"����1=���4�v��������b2�������4������������p;�������q����V:�U�TuU,\�V�e�\K�FkFi���������1�;����N\����b�=n`\$��v`��]���fs�k������-8'x�G��G2���������1��-:f�+������@����,c><��n?D�sr9C������������h�F��r��aWV�h'q��cN�owng6%v7��7�BfV���z���=Z���n���x��bA{�R�<�-qz��%$�RE���TR����I��*����������e��J�k`����b��U�7�K�{�c0[C��T��c'�D�0k�1km{�4���nK �c��B^j�an�&�4�v���������_&b�R�!�4��5��'���H�-� v'�c���z��{�����5lU��f]������dcs����pF��������1t4��x�"�j���:n�:u
Il�n����wVf���{_��R�a���������x
��QG@�|�cG�s��gGr����HM�������-�Y"�9�b���3��[v��>�N���)���frg�
�������Y�F7{���"6����	q3�V�[�jN�<�����|&(MC��4���S������{Co�s�C����j���>��>���5+�]#���N�L1����
�z*uL
��7������j{���U����.�-ba�.�4k8!LU����lH��rv;����Y�}F���Ptp�C��.�"j��2��}rzTo�����#/���?5��YV}��i�s4{�)�2uZ5��B���6r������W�8G��KB���{����7�������N���+����b=�u�M���	�c�laf���A�����;��o���!,��t�V�D��w,���HD]����$M����;n���G)Y��a����R�amkU����E������i���K������{w&��������L�f�<��:��[���#�����y/m�(Xu����j���zC��]�yC�-N����X��y;��vev�K����<fK0���V����NHb�h�zM�=��+o�S�8�8>n!�kd6�x�{~w�
U��v{K������{���DF���I��t��v�u���>z�����&6���6Z<���i�����y�[�E��*��E���>;��q�BT)gW�2*�+�f�'>iY����)s���������+R�u:/�����cAZ��s��^�=�owN'��p��V�����O�6��Q�FKa�5~����[T��^9�MU��wz^y���Vb�����9���4�|%M���Z��CUI�)x��{��p�L'en��~���k������G�s�J�B��q4���\����v�.�u���������,yJ��[Y�7�5<^^����|p?u�}����g��Ws3����=��x�=��]�z�
����u�rR�(l�t��Oi��Fw�J�_i�wn �G+Gj���=�aLWS�� �n��c��Q��	��[VRm��7z�����O3>���CM�ui�B��y�w&7�]���3v�XtK�27��{���6{����b����P��b)V�mo���`-�3L>���u9v��-	�w��;���`�F��]��.ucU*v��fX���L���6A���+c�;a����[��[����������������~��*n����NKv�C�o���������XS/�l��V������(�N�6�������7 ��i&m�m��Yu�S����7�'0��5���l��-���x�D����S3&K�zZ�9c6*��G��V5�evSl�:U�G)m�Mf�����%Wu]#8���,����Q*l��S?���Ji��v�wWY��.��K�.��S0�����'�^X���f���J����q���Ns��m�������]������U�u�P�����*�P���k�7�)���av@���:����
�Sw�2'5��b���q��"QY�.�f���f�{�t��edI�������;|	��]��S�cv�ho��i
��
�w�y�Wn������{�mZ;��jV8��[^�����7�v#U~s7�y-�������]�%)���L��T����|���wNWn!Ib�����Mz�����w�9|G���-�3����/��N)��{	����\��n��(�������D@�]�A�E����5�3k-�f����=m���2.\`��������f."V�s���x(^:���p��G
jzyD���a���w6�	�z��P���\U�:w��f���VQ�<��g����fx<�����}�����1��le�Ud3�t����>����w�g~���;���}:�:�^k�"U�^=�nz��j��`���T���h�k�C�i������"+�%FO�*��G�5O���3��v(l�P���x�@]��%�N����C�.
��z5QP^J�@�'�u��^��o�����o]m�b�kOn�N��T{������������^	�����z�@�[�[+ARp�nA���_�U�r�/lu�,�yA��}�qt�R�Q�O+�N��W��R����+��-&xG���*n��Y�w&"ZD��C7��Nt'=����Y���]��!��_MAk��4�vbjo�)��G�_�5
�>��L�;v���ybj\O%�����.A=����������3�_�P�������e>y����+*f���!H��Wx��A����&u��}f�w���xxV*�eiL��p+�����^�
�uE#\���'x�=��6��u�Y`�� l��k�<����ST��ww��;�.��Z�hl������������7��Z(�^�c�>��s�����BJC�={�s�����]�Z7[j��7����y�L���
������-���md*
�9����1+^:�qy����y���q�AK�=0�,u�9����?q��>�]���`"[�*�^�|p���0�k�����LF����m�k���`Y���X���LU�^�����R��SDGN{,EQ�pu�P�<��!#���OR�-B��z��qb��e�]`����Z�o}�7��o����]��I=�����{����-�����	p�\�NAh!w	��"�]��2��[�r
:,V�l����R.�8�:g�x�se�.���w>��Q�����a3�r�j���n}-%wEWU�H&�{7s8x���=�%�XEB������Qt88P����XB�D���S���P��	fy�	����p�U��k�p<�`>�'�<�rf9]39>w�.�q������.��<H����������$��DP��j��O�PX�Z���s<2��-b�������]z*K�}���1{z�UgLdy�LX*n�n���k���9o��x���{�R6|w����q�8��g��c�<�{��}���0�a�/q���s����������c���m���cW6����d�$Y�aW��V������|�:�mi7$aA���*���erS�65���.��T�"�7j�
�&�����yc3R����2[������Q�+N��-PnV�I��3K��C}{7N���8b
zS<�j�#D������&T�Y�����b�8����hz�6���zZw|��C�P�v��EP�1��X�Rrf�;�/�,l�xg�sY��p�3C�Y������b�wr�p��R-����U���
�� �#�=��n\o� �|��C|���+=�(PDTK������;���N��:�N�����P����W����!�F�>+��;�"��Y�N�.i�����`�Le�C5B�X�����d�x$������[��9T�a����i�������{FM�`)M���U8w-��t��K�����F{�����Bg0�t,�V�dA����4��;�;�M�Cll�X)�,�s���=�Y����������C�����#\~�k���7�m�(�	���.R�6K�uo(��;s3po���v�g���:���[���0��B�����s-,�m���4<�ogx��u0n;v]������uJ���U��zL���`��2�Xt��6��4�T��_)�tC�"�����]C7����,��6���l����,oV�e�_���y�����6����rn�p�;�5�Nu���cKc�WV�m.]����i�Y�lI:��{��U�o�7;���/e:yV�w��&Ld:/e3Ev�i����ac�q��t�[���A��0~;��v2^TW>x}�����+���fs{�����lh��Gg���a])~����g�5����=:�U3�x���I�%���2�mN��^��Th&����a���i���79K|i=����#����
��O����gmw����yK���:v���l���wj;t�p[-�����=�w����(��u�#������G�Y	' �����b�U�U����s�"��{��>[���I������������1{\�oS��e����)>7�C��P9��,V78�;o�����:O�Z����d�g[�z�,A�el�������>na���������n{����KJ�k��s�5n���
zZ���]�H_��e����/��
�8�D�
��T��=
;n�,]� ��=�zxg�#���_2�ykc�U\�����jR�.W`%���������w�����R�g��&�>/���t9"����sfl\^���`l�]<���Ln�����_#
�D����>���B+AK�wm�+BOf�6���:�F�K;��[�u�E�#�+*=��<u{���Bpc�A-�7�z5���^�zT��Y~�2��[{��	���6*Up�V��[��	�R�lK��o%��s�)���}�_����M�f��v������-�Ga/����X�����T�����m7m�ylyvf�����Qt�I���k���z�B��V;���rm@�>�����8�
d������FVAx��[��+0H���sN_��]����
r�������%,������C+�|�?�}��o=okN�h�;�o�!A'��X�rl����M_v��s�g��`Vk]�@w����8|J�:C"mb'~�F|��Z/�]3q�T�u���<�<�+��e�� 4����u����Y��y9���������9���}��^2G`����_��e�ypJ�a���I�Mh�f�+	�dL��LP�b8C��ON�:���w��]t@<(�n�����
�{��V�0�����F�uSB������Li4��V�'k�������i��%F�
�\AJ�`���m��}+��P�{�J��1�!��y�S�L/�{��gp�1�W���pEK��u3��^��Qn�����R���)6���Z���'}W~�$�5��	�����qo���xo�:���f�T��l�$d�r�v���2e��6����-���|�N��7P�����n&E�:����Y�`QWc��q/Ru����\�%�X�o3�bpepP��Q8��5�#�y�|a�����L��fyo)2f���t�)!C�$�])�O�9�q�V��Ktntk�g<)o�oE}�]eH�G�K��U6�J�5������"���A��0���������U�On�3���[�
����$w8�fa�=N�N���W\��4���ti�}��Y���5������0b���/��W�����c�(��ZY���ny�b����QTL
�����Y����`�
���T>	hb>W7W}��w�2�����'1������=�al��:p[�*^vmq#ri."F��,���9+)'H�}����b�V�����}r�����!������z��
B21u��W�u����f���_CX���z���:��|d�ue��MqU37�5P�G���f���U���5Ft������K�y2Wp�V�C;4�[��'�V*3}�u�;�����������z2��Sw%,$J�;W��*�6.�Y�0q��&����vaT��5���j�LL�D��EI ��/�=��[Mo��������������&���^���6���k,n���j�4�Vz�J�x�B�������`�/��l�K�:-���-���������#���*���t������'����:vL5��g>YKt"�:9fI��vm��D������1>�;P�&kU&�4��t�8Q�������P��-��n,67�]�z�v�����9�
��oJ/Pg�U��������eX�-��4��RQ�L��c;;��dp����n�Ue.��'��F�\=��{{j��w�Zx�wg
�.�4�9����u�b�nxu�-��L��Y�|�eS[��B�����^�����M@�m[�N��&�u�q]�����hw����������i��X���}�f�t:\v&�U��/�t��"�mj�sJ��;'��`Wh[~���
>������o��%�a�vx`��n�>��!�S8�
���ea�L������6$8w��MH���&���
�n+�e��*��s6i]u���Vb�z	�H�y}���_��w��v���l���m�\���qpF��S��	��JE�"��=8�o�;;������x�~�*��v�[�]�K���C4�����y��~��y_!��E$�������((��)J����������=�7��v@{�(�U�N:�GN���,�<�y����s�~�����.3v������+g����$n��um���-tr���~97y��1�4t�Mf_,}�����zI2MO�Hi\,�CW=l��__]�O�B���"��0�]t�<$U�^�K�v�32c1o=5v��N
q�E���
��2qe����gY�0zQ[���~Zd"������>��qw��q?}Z���YL�Q=����f�5��;�1���1�)^�G��yW��B-v�������5��`��<N�u����/0 �&�U��n�n�s,3Cas<c���r������@����m"�6V�N{��}���k��}� ����-����T�+�7����f�C��7i���9�
��y�{F�����r��������2��dB���U��|�Wq7��i�+�m,o/�$�5p��'v7�����xw+�
�KJ6A f�����e���:v�{]��(	Vc�����{w�b/[�g�[��gEzg�n��x����3��,���h�<�u���t#s�:��B�@��}�o��E��A���,��Kb�AQ���0�T��!W{����,�9��L0.�^o���g�����R��H�
(�� o�e<�V+�/���y��z�A�;��Y�9����y�P�RR�����
@u��
����h�]x�U�`��w/x�c+��w�R�L5F,n.N�n�D�\f���Z*�*/2=��B�X��K��/�����6�1�g���_@v�h�KQj�6�/s��E�t��������\�K��NU-q�p�z�N��{"�4[�`W����s��G�+���_c�D�k���7����4��G�Ly��+���+=��=�#>~�4�xW�!��'eV}�7���
������;E\����	�gvGq��^��^���V�k����nO�bm����]��Y��iYM�{'q��i����>���b(����&��j��q���l����k���\�}6��l�X��2�je%m�K��������Z�f�Y�Qh{S,���[����\t�Y7��iz�Ih=�bD�����W'F�!��&�����1*sf�Y;MA���K�E�����w��SmhYVXe�����;���V/�����2c4�0�7O:{{fzSW\*�x��a��?�\�������9v���B�����Q����l�m�%k�d���[�����<��mr����Uv[/hUPS|`�'6��K���f��{t�j��r���&��V`t����wW+�5���[kC�����fL3��cW��;8s�D)�R1�X��r�F���r�&1V���>�B�/rw�M\<�;Eq��:�s���]m�����\�����:%fJ��[�����6�����b��j�|��jM����P�8�n����K�=v��bh������=�T�N��b$wYF��Xgz��VC�����w��V���U�����}���d3I6���"�a���������������h�����]�{������^��a��g�8�d�0Y��D.tv���������S����4�o[�����3��K-N��gv�j����.�dS���s�;6�9�E������v
]����{��
��<�7�5-2Wx����Q<..=$::��UZ��^d{�o������F-�K�O������Y�O6�����5��������J���z���d
� ��	^e��+-�=}$Q��9�:FLV��Y�Wy���rL�<���;c=����kI�7e�y\��
�&Ad�����t���;���=�%R��Y�����c���sU�8���*7�Q���]����-���d/�C�������(q~�ti�Y��4�=���.^�����q�N,����W=*w���F����z�����������*�Yxz���vi���P��&)�@�����K�n�X3NJ���]�������f�&^��t�=������*V(JNe�R�	q�������N�����8�i����\���$)*���UO\��"���q���#�}|R!���!�=��� 
�/.��2��3����K%]��i�������!�
�c5�����\����J��g;D�����R���H��#�N���'�/;�fq�������x�p����.w+�����:J�j�V�'	����R35�K3���[\���j�����kQ/��.�K��N�v3.|K�Q�n��X�]>a��G����'j7w/)�'Y���	��Ky�{(�����x����/��y�����?U)��"Hv�1\���h��k]��f^K��C��7�����
�������\g*[}����4w�z��>������=�+���?S�{H�����	SP-��z�S��4��5y��FvfShM��^��x��O8�{[D��L�NuZ���mC���F���d�����&�/�>�;����#�����z�!����EiZ	�M��r����Mby��x���m�������`���]^������n�����mF���c��v�kgr��%NN&(.a��e���V�dE�a�lfX����������C��
7���}�u��_���m�
�U�y{B�������=�a�e�����^�����U�����I[��+�T{���cZ�B[5��_n�����s����t|V?RU�]�Z���<
Y���9G]����2�Ws�:��G)i��w���y��#R&�!�K��r�:��}��>�I�N9�a��������b�^!�{�/��X9W��=$Ft���k�P
��n�f{"��p�\���>��Q�Y-H�Ky�	z�����z/FZ<�W��'#2��}\N����5@����o�n���h�b;a�V���~�Cs��
��i����
�f�ydC=��-����J���y�6���t�,����=���G�����}�8���w_0�%�"�A\o6�n
�^�pFn�?r�|�{��]W��p0B��]a3�%_�s��'��`�V���3�	����9[�zv������x���U� �z�nN�'�"�q>�MNu��FHy��W)T�����b�AZ���e��f��y�K.�������5�I����O76pd�s8����]n�S��������M��pI4�R��s6��R����2i��>�U�&�g��%������E�OZn!S�k.��X��wn�6\X�l8zztV���f���;o<�^�C���j����{���S[�����{C��r���K\c����gE5b|��������Og�-5�s����l�ie$7)��A�pFu1�6�G���-++:n����g.::LSA�G e����M�*�������D�{�����#B��#��[���[m��<1���^�@x�������m����D��P�>��w����,g�-�b��a�e�x�o'��z�������Z�������cR�&p��9.eel��f�������^�Pb�F�Q
=$�l�z�h8:����������F}(�ojw��xh�z[�Mk})}���zE&;����U�yb�I��c'�!�5���s��&b;#/��
O�[�.��.���L��:�=��|E��|�iv����K������j�J�$�j�5�����:';u`�nB�J�b�VX]���<&�-���M/Q|�9U��O	&���9D�5��J�6�j�/c���|�e8��������Q8���n��U�s+
� �v�uG%af9=��}��Yq�U�{G�n�V4/FWh�����C����|+5g�)��5�������CN��
j?�^gX����Bw�A�oT�5Gv�5J��s�����:���k'k���~7��e�{ d��q�����5��<1T�g������dK1���w���vS��Y�br����#r�2�Q	R�N��	�U��2�kA�����- r��9N���Y���.�H����t��Dt��G6�C�p��pB��I��-<\��V3D�!�-����7�t��se�u8m�Ywqs�
��}�,�{]�+y[��CI����S������T�[G|������P��U�����[���55.{X�8�v1�L�c��#��������F�H��Y7R�l��F�j��
��#>9n��VAYS$��Q����&W|��X���u��,�5v9:9��D����
����~���I������t�ne)9��������f�O�b/5�g�1�C�hyks���{7=Vx	���Q���T���z�VUy�:����mc&�i����[~����D�&Lwpv���-���������l��k��&��U`LYA����Gsr�VM����Tk(\VGs�He���y}��X'R��ws2�@��8;��T���y0`��8=��B��x���gv����OQ��,�����j[������5	�M�����en�����Zz@G�b�v���wR
�.�+pj:��)�+<�K/��/�����b��U
1��]�EvE�~:��v^������H)"s�m�0E�2��k�_d���b'o��7�-�7�����.Nvqk����[���U��[���w�6������N[�
|i�i-�#�=�-%o�����/���8���MN�A���~�^����U�=c�]H�;+;]m��LF�Z���|�q)X��U9���7��N�@x����-��XiR���N^^iM�O%[���b+>��u�J����#�����V�:�k��=��y���ug/=^������Ph�0:�X�+J��*K�CS;Rn��*�*' ���0"z'A���v����bp���V�X4Nx�3����p��'A4���<�&E��!����H��{1�s/����@�I��e��N��c2���y�s4)����"�n��oq�in�oM^3|%nq�b	@�\�t���iV�[,j�39WZ����0���Pv�Fp�4VE���e���V��Sy��\[.���t���LC��_�Ub$�����z4_J`�#cq���z���|��k���H�6�����T�:�;�����:��r���9+
O$��/e�8�ej�]�;n�1p��W�7�3�-�Id����O��3����sH�w�;���������e�Y$����e�/�.�cS���E�D���#]L�x�0��st��4�M4��E}����R3�������2t���y�Y�a�A}X���9=�qw�0��$���;��S�no6����*:��1�����jW��fy��z�:<�'*7l�5;����*W����M��7�c������n��y.��B}�z�op�[��B�V]\=�f�jW*���9�n���6�:Z��|
$��%���s�lOP�fWT���X��?D��������mH�y��������Q�o}Yv��<�t��gm���X��FphT�v�
��=q9���`g��s�/k���W���|&x�52Nc�jB.nI��(�x6{3v���J�y�^���9���7���12������in�j�������?�u&�Pv�`����;�0_r��r�������`��#haC5���F�t�QoVN�~W9��q����s������+}�p�T��uy��;4��m�Z���[>���~�(h��v��.O$��������u>���e����=�
�G[8U%�����lH���nj�1jt���C����w
k��h����M2�6��c/�����j^V.P�N�zb�RKx��������wM�FA1�t������+`�k�!B��DEJ���E�����9��3W7.W��o��Br���n�w�JW{�������<���3�g]����G>�q�c�q@l��B=��������]�v-B���Lw��[����Z�@�����{�xy����s������G�i}a�.�r��,qZ��f]�[��+6����(��#o^��
t��H�m)�JE�Jrr�;5��=Sk.y��1��v}N�w�M�(����{_�x9�&v+HI������A��%��'v�S �x��J��`��q0�e���3I
�M�jQz;�->t��U��k����b������f����8"��^�Y?,	N~�����UH�sw��g�K3�����=�v���_x��4(&L������?���:K0��y��yP�_�vh{z^�����b�-O��9�*���_mq�1��
H����z��C
���W��C:O��-c����w����v%��]sh]LK�"�����+��s���nl~	�e�/,���}�&��fJP:�������C��G����Li��c����JO�x�k'D�+�Z^-B��y��E�m
��ywg���t���F�Q\k��,d�d3
,=3�L�"�����y����[��yo����M;I���U������p���l�sp�a=��X�"5z
���x�j�o<�= 7���=��rv�����Na�oWe���Yd.c�iP
������S,o���;����.;���r�opj�u��GIRz�=���n����MJW�J��X�&�WI�����iF�G3<&���a�{Q����s���8(��<��V�2_V�
z��K�����{<���{���}�;:�R���^�w�10�U*����b���1L������������9���m�0����q.��;�S�*�\�P���"�<�-u��=�f[lj/�f��2Vp����T��%pa�^�0fJ�J��u�1����U���7,<�W��nf;�[�S�N�m����Z�'��������'�`��Ne6w�(�(�3Z
�����n�M�/���i9�^��Y�_�p���T\�Qk�ZB��\$��t�����k@����g��g���f7�~��%+k(-��)�=D�@e�Q^x1A��]�����M�E5���&v�[���r�4��zC�y�u�_�.�/;��]_�O�^����t�p�C��4V:�Ze_9i_�v��dt������\G$r���]PfR��^�{}�'���(��:\���6���y�:��X�O`��0�u�i�Vb��3��Qo������f�.uQ�����:�~�B���4��Sy;
c�~j���b7'�G���3����`��"����7+D	

�d�5Wn9����y�����������0�<��g��ivJ�A�����^��MS�����F���������9��^�D�q@o�ocY�}%�5��v�n\*ns�����=8��>9�<�w��������v�Kg�u�V���;w)�,J�;�"��A��]-Y'���*k��
���ZK��Yz�5Q�.\���O���u��)%$����M
z.�a�cWT�����7��v����{]�2�L#j��B����2zw[B������3T�\��9����MG,>�����m�\��:��������JN�)��;sT�
���	�r����=��7Eb���s'#O�\{�u��E��^������/<�n�)
#�*�O�	�����m�:�����'fR�qB��������V�N;g6�)����vu�y,
�s�����j���1�$��-�]���q>��W7�u����+�ee�	c��eh�	o+�����+����JV����kI��2{�rS������Hj�	���6�q�~Y]tT)\F�
�������H�@���y�]Y��F��<0?R�w�v�<�n������\�)W�"�����y^��������C���	f��V�o��v%Sv��#��;��Tk�v�{'k;�_z���� U������=�2�n����-,�yebT�5m���w'KNiWY����% ��j�/�Sz)��g�o�8�]�Zg��2�t�o	�Jv9F�����f�/�����=�������(�6�KJ�
�{��a�/�7w�]������rB����U�c��7���c�s����D@+*J��V
^%�K���Ji*��g����V�yG	W*p9�l����A�a����Y�Z���<���1�%g.���ke���{�4�|e�v�H�����`��f����43��8���{If!��N�cT�t��\���{����4:_\&#�AZ�Uy�������^������k�����%YqoK�����_D����T�-������u�X�p�P"�����2W!����t��^���1������*;&e8��I��qk�X�@�<i1Y�Hz���*��z5����s�5�d��zx�������8=�y��k+V�; ������d+&U��@�[�J����OOT�iL��^
3H}�W�
�/C�w+��}�#��X�oF���M�Lx$?h�+lvfP��� ��h�k����P�_�*���}*�/��]+�}i�S��W��9��%�z��KZ���8z;Z��w�'z^?x�
�=%.�'gfe�.Vv�:I`���W���{	m��y�JxH���3=myf{
��!A��q������z����7B���e��V�'�S��vW��2Mj�&�4����_�F�1�P;|�^\��������Ga�x5wB�Eeo���+B�i��X��Hu{���SzD��eUv2oB��[��.�����'�������������������l(�����%}��c��w������&�lA���������e����
*���f^��U���PKO����VN��,1�a�)�a��&�{lQ�u�WA?Y��7y�)�`w8�b+�5���}����m��C4����V��Sx�d�e���4
y��e;�}��q����2-+^����p�U�$�A�hM�SD7oI�C������_e�Jm������_��u7���N*F�c\�C��{�>�=�(	��������O�2��j��4a���/�l��������U]~x�yA�����llt��/}�� ���f���,����|��;����EU��������o��>�$�o#&��ir�5V�t��t"Wt��<��2�od�6a+��s��w~';��:�R�:�f;
m."V�':�{j"I�������5l�_>UhWz�����@��z�Ebt����c^����p��m��/!v-�W�����k��t8c��u�����������v�u����{)��F�>���������7};���g9d=�0����2f�0� ���y���z{���^�������]�����Bs������/�6f�(��sa�^��n)Z���e:�L1""�X8E�o����:m����:�P���.J�-�
��v�P|o���uqLQ�����u��'��^�����=���T����W�b��#~5~�z;����;k��)��G[8#E{���R�m��k�c�����}'�.iI�g]j#)�h{Z���z�h��e����w���}TI����6%��Cj�l�j��U��������Y����"/�L���>w,gl��J&�{��P�z](�����v�nu2f{����A������}�u�^�YC8�g\]�W{+��V}\������)����_z�\�9���h��Y����T���)�
�x4
T��A]F=H�<����/K��u�nh63l�d�5�v�k�jR��"��q�!�;G�m�Wq1]S�`u^��]����������=�g;������bU�_i�����N������=�G|���J��,uxH�M�/���q��[^uw�w��EW��e��W-���GJW^i��s��"V�������;Pdc� ���U�IPF,���u^�G�R��w�!f�=FC��
J�
�=��i����.B6��s6��~z:��M;w�nY�c7\U�����-�T��N�n�Z��b3�*����r�d�\u��=0�����g]�J�4=�r�d��G������UI��)�(����[��s�QC�����3�-���������<u%a[���K�p�"�<�9�>����&1oF�o�����S��g^�Rt�BA�u�r3��D�������������><����m�L����z&S{��+�����&V�7�s:���������o{����y��}�J�'+i$��r��On�B��������#�_��Y����)�(
�:M{,rV7�K^�
s�{�f���^����^�)(y�^{�^��e`r�d��.z�
EZ�^z�9����P�VF���`Ra?��)��^��?��yu��X/%������>G<��	��*��?�������{^����X:����Rr�N>��ZV���
#	��v���\�Ka$	n�5aS���z�n���\����zL[����y�i	x�#�!�U��v�����q*��EWegdo��r�h���F��r����Y����pNQ	�����{6�����	����R�n]�62��X�����Dp�R��ZH+����`�z��I�v�$���O}�/Ej�������n��3����F�u�X��+��1��5�;�������Q ������ otT�Q�ez��gxR��X�v��������&���>��`��A���T��W��s����G]w������6�x�C7�u5���7Ms�7k�Q��TQ��SUhL,���xD"�QdV-�9����v�]B��|V���^/��I�Y�0ZV������[��=�yH��T��J��~���`SN�+�n�XD��w�����.S8�4�e����u[���y���&���H!�**�j��i��S�C+AER�g��}\5V����nX3Z������{�_���o-��A�o7
!��]��o�Csn]c�u���XR��hR�{oL���^��5BE�Ww�2��&�S��S�?s[�_q�h�R��B���)��sj�nq�:Vs��{�Z�8�����1]���P���a��\������n�a�%��P2��`��e^��mox�����YX9��.���wlU+���e5��Z=B���)�]����*j�q-���E�[�l=U���4uoQ��av�no�4l��kp�oa��q�O/���|�*9�.����Qj~�Iu�o/'V�.|w�]u�x.�60��:�T����d���St�c�,������a:���������&]GN:b��[���:n�}�?*v�-��f
����9b�K<R^8��Vn����3���<��m��������jLm�-��6.�]������$�1m@���gw�Xy�$�s��t��U���=W��p��2�Y�2�WOmL�Jy=>]�
��<��T�#��>Y�;}f�ZQ8��w �'hd�=���t�x�K�e6nw������@����1��#�]��
��[y�gJ�S��'VYx���P'�\����
t����puw]��s�u���Q,sQ0��e7P7q��aY�)������T������Y���`�h�j�wa}�6}�x�S>�?X7]4��+��������
��sx��dD.e���5X�����k:�9��"�e��X��$E0�f�����ibO%�
w�M��M7|*+�)����M\[m?fwgn�Ufn��Ov��
�C7�p~K�
Zg��~>Y�Z��Y��]����S�W���n��C=������{}�Y�z�����#��S�������1e���N����0����qn�*���f5�q���xX�ZO���`���n�kgX��U�B�*�P��
���<�
����ww��vv�P-�s���u���c�]�>�uXy���~�������K{�c�u�q
���=Z#L�����U�eJ���x�w���a���,�z��v8;�=����s\n���i�Qsp�Y����G�i�80��r����\)�Z��4��-?��nb���q+Zu���	�D*6�l���^�g���"N~vJ
]�Y�z�r'�8M'��n1��������c�|T�Z�%�j	��9��=�6Lt:��]���be��3�g�s���q���_������31(6�������C��X������1{.��B�k���5���LB1+��l[��ubRg��������@R^�������!�@��z�1gV�]7t)�KW�;i��7|��i��Qo�hw����~�.�Y�lR,3 v"��w�������W�b�x���S�������.�d�x�{�b�[��^u�{�;ea���hZD.��6��������F�O0���{��9 �py���B�3B��3{��PtL��]������^���'6f��,�q3t(dut�a��<^�3IsC/fl;=�|�e�<�)��������B�u�s;�����Y����36Ej��������Y�����[���y��;�����W�V�#�����=����9����BH��nGg<����6���J��b|�����V��D�Dv-�	e�����c��Rx��GE,�/�6yp|`��t,$`�R�5�!���>2����*U��
�]v����p���ss��x�����4/{,��
4�N1{���^�%TU���m
�LB�\��j��u��������:�]x����fVtI���u
k����eb"2���Qb���6w�KE�59�Z���Cw����Jl_L���t'<�-����i����W�Y��r[�n��X6�4����f&����L���wqR�qJ�3�.��wHr�
6��WoO5���������v���Jy�GGjs~�	�S��y�,�*�8\��zWgE���@Il88�t���^�k#�%����%���\�OV*#�<����3����ZIg�������	�k;�/���]7��L���6n���l��'�t	��rYd�����{��w9zKY2���Q�c9���h
��>���qM��Rw�xu�wJ�G	q��G�,�3���F�w�e��5}yo��EaM��Ct.���T��
[^��$�:%#������/����I�4�.���7'+olE9�4�������5�\_]��������.�y;<��C&��=��(����,����b��o�^A#������w�M��Vb��n����A�Z*Ifl������Q�2 NH<�@�40�U'����Bm��V9
�����������I�q,N���^���f�
�%z�Xo��������VHY�#��(�A��J���d��B�sBa/|.���K)�&���~�p�d��������m��B3��8�5{o���;h�8�q���."w�����}����u����]F�lh�9gd0��8��tR��>}�"t�;6���vj/QQ���k��%F�����;='Ew��^�w���s��x���j�*������%����[������aq���fQ���q{}���k�T��&b�F��rt�L�:���k�v"��]N�vIr��/<������/}��j]���x,�WT���CW���A6�mjq�������������;q���x�(���"��ji��7-�<A��.y��Ob����T_=����{���������wyy��:����OQpg�#o(y���tl���c�~���t��P�8�\�]d,��~�8}��^���7W�S�����e#�{�����W|Gw�%��K�q�Q{���U��.����{���}������pf���4���~j@���7:�6��3;���;kh������Z�;�������������n�\����&�������'��F�q�������O���{������D��������{�j���V�lg��������+sxWB����0��.���W�T&������(��>vu+��=9�`���T�0`����^��]g�������v$�/
{V=���:�*���%����{�f�}G�G��q�{������U�{	y���A3�����^f�^�=�
z��8x���{���\��9V���n��V���!���h�����2>�������s����+�w�O:���D8g��n>U�D�Ac�Wz	�_�<�xa�u;���V��ON��+&�L�x���Q����sf��$Zev�,���f��*^jv�{��*lP{*m�cp5C�QT�?�w��4�����mH�*��9����#n�E:�KlM���6��x���~�u�'+a1�=����c���Vj��������U������A�f��r��1h�-���I.�����c�R6G�����{�������tz�*QD���v����>�1�<�5�[�xp'��g�d*_yA[4�����������,yjS�V���a��E�QX��s���y�lss:�P�/h�{7�m���0���S�Z����������B[������<������H�����f����J����(�NUz��/��7�]Z�T'�x*��:K�&[��I��y�#��D^�J�4�������t�y�x����g[#7��3�gI�Cs�_0y,��n�^�Qu���5���"���.��[T�@�.�<v��u�x;I{���(s����������Y�]mT�V�Z;N�!I����������7 �[�c1*���A��m��y����\n��3�M�^���^�]��B����+,��m%;IN� ��0p��x���[��)��|\�����$�<y����3>�r��Uh+r,Q�B�9{GF�w���N1k�$>����v;�E��P�����������LQ���v*5�u�cR�='�d�{��exvU�D���@�$�����{��y`
�u��B������PIJi����w��Yt�����8�Y;Y�wNe�On�e0��a>��	��n�3��w��#3`�B��<:�S)a�%���yu�Y�2�p�]O�|4EN-�K`��m+�Y�x�'w�<��D��I�
�����r/�Y+�6J�Y�{��l�-uL���:o�b�jZ�A�25�S!�/��^�c+�\�u9�egTW�����q�3/`�u�;0�������{�?{\����6x�������.�a���'�9Q�E�O5�0fJ[����o��j���d���w�'-��3V���%�}N6���37������e�r��U����j�"���[����<GC���k���`�ru���VL�x�������.s6�!��J������EQ����bo�%2�3~����!�H���`�����3�o���55��V�V[|��@�f!����P��~C�)���p�AW:]�r�g7�<���f
�S�
��L�;'���������L�{������iAne�:��������+*��V�`"�Q���w;�����n�������j~�a�V�~����:����A��a���n�����/��ay�{��iw%�z]���������|��-�{����J��{X�{��U�ou���������sV���c���;�t�S^z�K��q�'����t��&b�fPs�[���gM����T;}���cR����;�c��f�{g`�3�	��m�	|�w�t;��_�|��o�E����!�6���F���E�y]��^�e�)��Y��}zu�n���~���Bf�����V�e�������,K=[^h��5m@�oT��X��'��4��=�gp�o�b�w���5L�
\�{�Q�����d,��E��������l�)��7f���+#^\�U%.K]�����B]N��L�����=�����v}�*b�"�u����;�w1+B��^~�C8���xu�C.
�w~�>0j^�y'�/�\�2>�D;�:9��Z�Phx��)^U��}��5o#�2g;�R�Z�\%c��uj�� ��,�G�4�s* Q3�U��VYR�����e�R���;n�����w�=�d�Z�si�X�y����
bk.�0�[��`~�^��a�P���=�En�\��[��J���k��������u����P�������m�2�3.*s'"�������.��V���Be�K.����??M��
j�=��,��$du~b���.,TG`C��G���U�d3�I�F&4���9��`�����Sl@��I��{d+�:��{ ud��m�j��Ot�-T�Z]�����Fu����8���S��8)yKT����Gl�z*9�"�R�`��[����v����K��.���g����|�==��EO�*���X�\;�t�����Od��;�%+�b��k}O$r�8�i�JU�tIH��W��~��L��s�vH�!6���]�]���_g���;+�@�+*2�����z�NTQ���W�OZ����k���������6)�-r�zx��u���9;���E��|�������G�.��_���3�e�,f-�����>�����Q��%P|�<����
vh����|���y���Q|�����9����(7�*>�����A�)���D���O���e��|��q&���!�V�J�1��������������)���t.0��]�$Au�;2��z�1WTp��W��'���:�[�cxeg3�5y��=��1o3�Sn�-���������u|���w;�x��s9�)���i�|/;�^�3,�{`�y�G�?����������>w�<�#���R���c��]n�mc7�DM�T�+:���(���ty$�R7�.�^7�������
�D����i�9�����|����0C��i|U�Q�v��=u�>?o�>����]��}t��Z�FF]�j�b�*
�
�������~�C��}�\�R���X��0���[�1h�3��[8�@L�l�X�����F������XX2�d9[����xR��v�v#���S��O�~������U(��������Y-e86S�5d�y�#G��YV�p�s�;+�+��+a��r(r�5;�GgH+,j��}�!v�D�3�V��0v�$d�CGyx0flP�=H ���h�Z.��x'J��	�/�Wh.*����.
����]p&:���H�eM�}����w�kO3z���rX�s���\Su=p��(� ��-T�U++p�\�v���R��4����5:�y�
c������4*I����������qz�WM�0�����u4���^U&�+yrk}�(���1�c���
�\B�a[IE��#�������*������`�n�/,�XZ�WS���%�V�J�{�|

U�/�����))���
��uY�W�_�S-
�#��)mi�d� a5�{y�0v�%O�����a��]������u��Q�����j���,&�N����2�f������+�x��Z���03��:���uPK�WV�������'nv�:�H0&S�'���Mx�~�p�2���X����:�s���/c������[��b<��{\d��]iJ~��{�vtkw�0Dx�!4I�l^�4�%���f��v�:��!��]�U����M:W;MW�wF#e�6�d��a�/���c��AYy�����#�x�ar�4nk5}b��y�v��<�l��B�o�����U9Q������kVX��Y�IB�����le�Lo�<U�s{�O�M_A��_hj�"�����{Oj����S���D�-�����,�gQ�8��O������=�q�M�����
������Tr��I�%l����_�k�p�T�5��+��'�z��qz=���9`p���k��<,
T�:[~�+�f�7��%�'�N���7�rd������g��;�\�R���7��S���H{X�,xY���bH����� w+�
$���{�����[!%�����Y-��9Hi�I����_X��3L�.S;Ie���������M�P���S#E-�~�r��n�q�z�,=�t����XWx4;������VvU�G��]���QK�[HO�oDkg7���7C�
��#����%ht�����<]�����)�f���f�����23������^������}~�;
��&0�u�^��q����b�"v�|W��d�tV6�y�������9�����[�2�d<�NS$Y5=3�Z&�bNM�����8+H�+-{��ao+������{�g{<��@���
�%8x�3eOn���',��)Ov�(���4�R��I�f�jbS��&M6�n�M��Y����78th�;7���W�\!Ko���EeJ�|�j���S�8�0�`s8�U��X��S��\�EH��:b]���5��������q��2����w���]���N��%Z!�A�.�GN-g�*A������w��d��2������=�i�nD~��g\�j�������dW:�D�������}=~[ e�����
Nhhr��.�������ey�9�����~�EnLd1����<R�Q�����s���T;!���[��IX=DR��E�:��T�$���r���[w��f���r2���y���)�q��nu��|����n��E��S�������BY1��f�*��s��(�H6�O5������k��s)�g/b��3����YB1\F������a�)�l����8���k�����h1�1w�J��1l������{��3o��5'e=�J��{����Y0�G1���\%3����1���y%D	�(n;[��P�����^���+�} ��rw�e�T>W~�Z�^���6�s��
��]W
�T���{����9U���x��x����1U�M:�����3��l����EUx��)w�U������;������n�<+/��%�|}x��,)��M�^�5z����6q�R��9H�K7��;])���0z-��~\�$���ng[.�6)K�Sf�-q������w:\��W������L���T�S��9N�A��x����v&�k���w[���7�i��:����l�q�.d�o�dM�n�������m���-���T�T9WU����"������(S������"WC��=�&����7YZ{������QZtp��~��B��{����2%�o�"�Ve��SD^n�\ �������O���Hc�8���w�M����eWk�?
;��h�����\�?���o*����C�e9i.=Wh�1Ju�,*S��f�9��h�)�'�u�fC�r:���Pb���2����M�W���]��g�=��u������G�c�/D�d��;Y>�Jmc�4Pz�[���|=�������@��e=*vO�h���R��uzf������;JM�>��5���*i�,��&��h�7�����~��[��WN�7�{��i�S�%�8)#��S���n��n��K|���F�RK����P����>�v/}�N�8#��[��vc^U�%�RYS�PC�2�\g����yW�[>a>0��^B�$���
a>�e����D��d����:B���w���r.q(�K6�9��B�tX���"'wV�m]Kk1c�8��"��Nq6Z�]�`�aK�av�Z�����a4<�EEO}����mO%h���sQ������/o��L]��hQzN�����qcu#�xyS��%y�2��)�=uX��T�U<{�3GB��LN�aGer��<�n�^��;bj��7�?xt�aI�kj������,Z%M�Ia���g%���W���mi�g�����L�������(��E��y�j���Mw]���>��V�r2��][.��ME����xlec����w9�l��a���h�T�heGZ�oL�1���<}�yL7��^D`������|�bQw��{Y���T���V]����r��s�<�Ij��=�g�����X����n9S���w5��5b�{N�u��(q\�~����f����;�fX�s���;�_�����u����6�-3I*�`����4�E�*Pz������sv�����>��Xz��!n�L��������^�����Uo�S������KoS�^���:�/g��~�;#����3��zZ�����9��������wt�N�E�@�"]
��!���x�z��8U�"�Y`��=F�^�T�,�W�e���`��^�%��l�\�}+8�^nLK��4���^O/Y�A�Pg
J�>�B���yy�������g��@T�j���+lYd1p�I5s�;��F��;�����g�gOg���}�6����)G]7Rkri�6�9��R���9�$�u^�O�"����<�p	g����
5}@��;���h������!��y����7$����y��7"�~����n����y�O�h<+�1�8:�}���n?C��&�UMu�uu��+G8mT��G��!J�wn�"Y��p`4�U;��[�kx�CeN����caN:�	�e�r�&;u���x������
�P�W����U[Sv������+����/��>���n�,Ar��/��C�~�����v���w����Z��My�.�T@c�V�DOR�}w ���C�}�l�a��2��cs�WU�t����z0w:n��z�f$xS����WQNS;~��SX�����r����O�s��D�������Uy���FM����9<�9�{2���R\�)f�9F]xw�
�����On�W�h�}~~�P�������h&�����3wg��#�%J�w����;��t�Ew0�blb�x��F�������'��V"�,9�UYGS�Uj�g����j��E�^W����*�ZU<���3�������S�e��m�o��*9Y���T�4!8�{�A,�W�!t�����	���/���������Q]5�9�
����ag�!�;��Gm�j�s�c��0������c�a{*A1��
�;���%h�t��}��w��T���U7L^??$����{}��o`N@~}�����^^E�6*�+�B��X(z����N�J���Nn.j=1:#��O��X�;��X�\�*���/��l�C�Y��}�Z.j�/8ck�P��R����[�)�Dx���wx��n�7�y�z,�L���>��=��r#�Ym(�DG1��5���{2�\Y'6p�b��{��<����)��3:�u)�y�{���Gi��-�{d�N��N��������y�GE`�d�Wr��u�C�����_�
��pJ�
�:]��U��^��q]�qi��$,��*eF���0�H
�t���?Q� �z�V��n�K(Q�f�r
[�qX��\�D"��!�k-w�}N��d`s������qM���X��-Ajs�a�s��mSz�^��m�)�V�au���E���}��]=�����9k  �d��;�k@�E�W�!�0O/d2-��7�b��#hb�A���;O�TK3s���,��v��u����m��;vc����
\L�W���Sj	��������1�[�&���1]�*��o�^2T����7�V���C�0��(=������;N�v�{s
5�,��0!YM�j������R��A��c����[�gFpzUmqR�{��r��U���t���^�����Y�i��
j�<�1��Gr�)��vs�$��<.��s]���$bX��)�X)
�\�+��&�.�4�����=k����R>��5^��K7B�x�QUe�M��G%e�5���9Rq��y���g���ti�o=�����=(��X��+�<��#��7�^V&
���eo���3����u�.��WU]������/e�1��.�������5*��LIz{v�;�p�F��������s�7Q�Q	f��3*Ax�=��#VTh���������Ewl�\\�nd\���1�����x�}ub������=�]��8�#���N/l� �������v�1���N��$3���@���'zQVq5��7�7&����w=���z�������1������-U�K�������]`����u��m�
c}��Q3]�}��._f���;����r���q**�\o}]����U�:�|���hh&h�����o
t�^���m��3�z?�e�]��a����J�i�N�3)/��������J]�Z��*L�_w��C���C7�<����%��^��t�����v�Ck	u����)r��QXY��c�e�Z�3Y�S4uza�����N421�����(*{��^M���4���Etx�f�n<��g`�r�b�O�}���t#5���$���U�Q��}�9�j[3A���81m��9�%�q���M1t�\F�+/���q��[&���[B�B�bC��S��}����;��%���7fgT�����f(������A��b�_�ghA��(yw+crEF��M;��T����b�J�C������K�����=��b��Q\����Z��8�,I�������3o�E�<�-�t�b�������U��y3��j�{���=O���-����{}o'bk��y
��z�9Zloox��;�+�B��X���G����g�����LJ��8,`O����]U]3%��+�����x���9�\V���������{�)`�nRW�������[E��<g6��6�r�{On)�<TB��Y#���FN1S��N
��]�r��\'K(fR+��G�)��kv�m����f���&(��-���jB�T���fK��_7.��AA'��z�z{��t���	��f���Y��?N��@}�d�{���P �L5����Uy�<��'���J�M�������N:�u���@����_a��q������������Mfr,m��qV�f�S�=����
�h.�SQ���~�����������>��V��U)[i&iit�t�r<���L��9�<|�n������[X��c�9x��.	�h'�}7O��r����j��:�B�G3�sz3&��������8^Z�Ts���^.V���W"]��]��u�;��]�4M]�7�������0j�ki��{��-���j��q��������fC��Nw]��DO���<&�#|q����=��dx��<7Z�:9�drs1���c��U�:)���)���8z-M�5Od+��[P�Fp��Zj�h��Aw����.�g����_t�=��eG������������������9r�tr�s���I�\����7����m���[,p;U��N��z����R|���Wr+qU	x�n���)�#)l�����mw3�t�Q��m7�t����%W�� �A�K.}���2+�k�tL����vT�]�z�X�W:�p��o��5����R������� ~~y�o��x2��>�Z����S%��������u <���i�~'���U�;�7����;��w���{�7sn�LW���Y��E��Y�\�5
h��=w���*��P�S��d�+���'��lJ$3^���'G
�ls8~���C�����U�<���y�����H=�6��W�/�mm)�s:{*<0�l�5y�R$����1sc&#]N�Xan�yKWU��1I�\x�<mEp����S�aR��}�

Y��f��V_
���X��m��M��3�}�hwwG��&�B��;��&RJV1�j��Ze
���mS}9�*j��\7%��W@Ax
l��������W\j]H���������9���2����ys4&�I��-z_VrV\A(���
W��"t���vQ���Zw��_zWAX��f�1�s��\��fnO���j.�Z�q�ajz���G�6e&W�/��Ov���\&��RI�M��������I���V� �O�^C����H;sS��+�e�]V��[54d��kCer���:�zkmq"����B1���f���[oN�V�l�:0��*�.���+s����b�����[Vm�$m�
���_���#2-�*�rC���N����e'��K��Z'^R�),�s�2?V}zZ�4.��H�o����d��}���S��79|&Id��b��Gsf�v�O���Le��N6S��_Q~������*��3u��4nv���J�Y�SQ�U���k�F�\����m�$���;����Q�����
u�u�r��'S�u�L{e �u $��[�`�_].����������^�����g���%Oy�t�r�6��
�n=��bmN
��>L�\����h�c&����sftY<3���s>s2�k~'{�n�d�+��E*�Y����]�����3���s���RWe�����
w��Yo�
����G,�9���v
_�v��q�CH����3J��Z�����X~>�}�)�*_��U��������N��m�����4Zz-��q�OtG��yco���S��\��g$�6A2��f��s�M�D8��^�0�v5[ �7s�W�&�(�3���q�N6��`��l��go���E:;U|4�.�d���D��k
�Gj�P+�����$��.X!O��X��}oN��\���������N����.�Kr��I�t+�:
��aq��h����Y��	!�_[��1C�A��;qv*�(��R�&������}R���5�x�Y��yv5}�{����PRfG�:��x�a�����I� ���O7�7���_-�����6e"���V	����l�])���3�3�p������q�RGaX�������9L���y�J�	Z�\(R�C�g(��n����<��R�2��g�a�s��R�\�Dm=��2fcwW#YKOC5m���,h4c��z��}��m#��rW�C���A$T;=�>�e�E�=�^�U�����z1+A����/Ey����R\�c��V����mS�����B��52�bHffQ�X]������|�N9��EOg���U�����7��k���! 9�*��O]M��
�$�g�v��[�9q���T=o\~ar�Kg����5�t��Nl.���������t��>�CS��@k\�m>�=c��r������������V3�v��J1m�{>���%��U{pK�a+�y�sTA{t��v��@%�����m����
��`�����@o�����p</���N��L;[������cG��=C�6����x�R����j[�����=k�DU��.��ZL��G:����.��vyM��u�`��R��Nm^�1n;����;���7�C�{��B�7{�{}��EB82��si`��)��	�e���kf����CW��;�Rf��i��*92�#vp;���
�Xu����O{T#��7��d!�?.�2N���r�~�xz�m���yo�c2��m���rk��*����BX4��m3��BNi;������j��������V��d�G�j���IM���y��)wg�{[V�>��67�]��my�U�A�m�%�]���U����I��Z�l[�P�3Ls��7I*�<�����f7����x��E�h����.|n����[gL�r�"#��"���+�q��f���������[���5���
��\���`�>�T��'��}�
�T�<yh����/�e;!gA��2L�v�^�rK��
�,�>KmT��u{=*[��n���N����|��:�s�����\8�0'���H��5�:d�������u�������L�����~���;��^*��^,'u�n��#U���9�^-"�z2��9A[�����z�_��a+�������"d<�o�
��Q�wN�'
�~]:���\YZO���UsJa5w8�q��$C4w����o�Y��w����s�U����vZ��k\/�f
����&V0��yL�9[73��F1�vA�%}��\���ck�/Wd��C��(����_��������u��yai6������i�.Z����o��#����1^Um���J���J�f��4����{(�cf8.&2���{>�����:�>Ye�R��{bK�9��.Q[(�����IZb3�x�>���uM����t�����w��`9{Iw\K�]��5�7VD�{�gX��������3�(�xW%�."�p��Iy�*��������}��F����E�����L����*�p7�zW8���9���l�O�-�x�$\f���:��I���a�����mRN���\�:�S=���J7M�Lu��O��]1K3Y�$���D�f�������z����
��s1����X8�}j�}���������7����7�;�WM9�89Yl^����^x5I��	*��9Z}c���sX������Q�#�~���V�2je���7v+������Nc����o����,4t9b��z�V���g���j�e�����D��*^���V������z����O,5�NV+��b�E*���q�)����;��[]r��8��V�9A;W����$M������w��U&�vJ�UGL���z�YJ��l�W�����%����Z������^�
�����1��� 9j�Na�]f��=wEo��WGZ����h�e�����t�8W-�w���v�\��Ro�o+(��=�p��:��<�S��H���%E��7�:�R�=�����R�\���`�8���;&j�i�u�S��R3VQ�Y�����:�D8��X���o�y�y5��+E�����tM��x����bB�w�(�F�.$���`�T9�F��=�[��J�\����mM4����j3����#]+��e�����������r��0�}u�c,U:]^��k$j��2�����)��v��D���+[�,������h8�%�W+� �6x��H=O��Z{W����n��x��i4�O-M�R����(�>{�B���{Gu`(zDt�~��=g����KP��x�������;���P��d�<�Ou����������Y,9u�^��(�6+��u����/�����2+��q����K��Q�b�<6���=�.��y�~�Jmz�N�J��.�������V��x��B�S��w"s*�m���Omw?!V�q+G�j�gK��T�(�m��&=��%�;#��7]�86*����v��j�;nF��h��5���@5�g��,�+z���sq��2��&��q������hWF�;W���tk%���J+�`�|������H����Kj���V����v��HT�7�*M��N�Z��7�rz�+��ac�g���:���	n�0h�w=I���.���%�Q=�9��(��w�wSy��t�����z��u�F�`�H{l�2|Y�V$��t����YK��w/���
�p�:�������|H���+:��2�,��3����HW����Z�1�V�2���<������@�Yn���j�g��m�W���q����.��}{�HyE�7;���f�^��z7yu5:X]���b��W��y�c����d��&p��J�l���^�}&�
�_xi]|�I�U��Q
E�{X�fd�y\����s}�J�M/�-cgq|<����tT�������u>�$�����/�'���eV^�u���b��K���|�������:eM-5��R���z�(BB��~qL��}ya{i���I~!+'u+���>��Cw���D�sY��2�\b��VSX�������N�(���j�'��\(�n�Aa���,%�X��V�f������y�=����.�,}��lH����P������%K�u����5��Q#��Js��79�V�M��"W�)��2�������\N��nJ;�p�6L��e10�]d/V���
�OwX�}���������OT���OGf���.y(+���k�dM��_������]�~1Vg�������Qr�:����������Vq�j���S��}���a[�i��+�L.Z=�F�K7X�����S3�=m�|�����YK#m�]3DBQ7fL��h�o4�#.�bo��
���������@u,MBhu�������+��g�����7�P��t:���T��q���2{r������O��{��)d�I��]�+���QQ��5��
�TL;��s��W�����������x���JZ�.��v����P$�����3�=9�K�[7u.����eb������H|�9�9/�����3S*�,nS��}q|9���\�_�J�u��d��7�����&�:����l�'�-T�7u.6)��\�����%����-u�Zg;e��.\�}��f���\7;���	���"���z��o���|.�9>����YZ���#�2y���~���]5�$"���g
Z��E��b�<���y@���=��e������w.axEr�.or��},C�m��D��Q(�)�"�F@ry���Q����z��3�������]WC���nY�.�z��2u�b3�f��#p�����sIN��	m#�C{Er�X<����+!|��XG1����e��8�LO�>�6�0��1.��sP����\g�a��3U(hQ�X��\7%��s��y��/I�0�b
�LW� #]g���j��prz�T�k`W��eB5Hm:�mV��\�
�Z���Q��{b/U��]����=^�k�_L(��zO>3�Y�q�Q]'D*���j	�����EGP����qRf��}��F�!]�w6���R2�T��A����XYG���wm]���g��x!����W�7����g�p����'|���W�r#���{s�k���v�j+�4L!-�2m��_YOo'R��z�G�����k�]����XXe�Bk9������ i`-T�m�0�����K�L����	��ygM;���P'A+l���l���t�M�������M�{�-s��E;��&�%R���0����q���S�Jgy�<C`��7:������&�J� ��'P��poM�^+ysW�.�uH�G��YH���l���S��!����QEX�F�!�k�8�gn]qo"��ye��������s3��=��kI9��������Oj�F��<R'xt=�������n^��{����F.td���mn{��%���B:�Y�u�QQ�I��7��5Up������&�q\��-c��i���?���x5�&�8gZy�J�4��9m��FvY�K]Gyv��HO��Z��F��;,a�4�R��{����3���a	������}���N,�m_5+C��&�SM�v��Yx� `\lG��wf��i{�0�4�w:b��{1�����r����������@�������[�����q���0u2�u��8��p��)�]�C	��}�a�w�To���_K�/]����==�����&s������v+}4]#��D&f��Q�������P>9R?M7��Y�X���P_r�d��Y|���ln����8u��n���f��i����r���I�Iq�+'�Q��A4��bx�d`r�AN�o�b�,��w�7]����Q�a=KD�WZ9Emwe���9�����/� q�^�F�XVw���v�l��N���}�*�����S>�)��UK�0��E�gw�tA]z\�,cx���x�^S�|��]�d�O|����� ����0�����Ue	7����q~����~3��:�|]��v9��Lt����
U�5XJ�����r��
�698���GXQ���J��1���n���O�$������{3�$��C�{�E/tw%J�3�DV�����Ke4;wt�3j#(�&����)w7�������n��{R�tt�!���t:
 ��z�������o���t���.�ss�T]KJ��*�K����N9��6�U�	���1�x\�dv�����x�������.���1�����#��}Ed���-��{��Q����jY�F��[f1��w�Lm��q��=�;�R�4m�=�T~��>������l�=HA�:x-���s����S?-��0�)�;<��}�N�4��<���s3�|)b��O�-�b��d��N��5)L��W��v�D�~�������\J�����;�\6��^�8��Bu"���p#�R���g���n�m��U��**������=���{��|�$��9��Y���B�����cm�f����sh�.���q����.���&��u�y�����fX���G*�?\��
F�������+A����$I��fqB	�V���
�h+;KzK��3m�N����o=���^Xy�4�oQ����-�gL^��u�gc�n���_W�����n3(��;����:��#����d��PJ�v����5��}B���q��R&��
������+6����Xr[�3�N�*���T^�y�E�����%W������fMnCB�W�G�PO}����	�W���}�c���l1�]^��;^�o�!(��&H�*����T����?���7�K��9���Gp?�A?gS����l�K�a���,����<or�>�-���W^Qx����]+��0��:%{h��{�f���7qk�����M��4gh��������$�=��(	.<nR�N��:�=�l�{��NMi�'uS�)/*9��o5�u��o�I�r�m�FF�J�
b���g0�v�<�)����7��f��d��sberWQ�:Dg$jm���?��I��T�����
\F�]�!�A�whc7���^��}Ic��8SC���E;iU2�ec�V�����I����oV���w��o�_����iQ�Zs^HX�G����=��3C�����\O���MC1�����T�����'�PL�F���sj�5��Y������;a���x�zk�'������zd�4(i���AVs5c}�x�M�r���t�aZmW�yvN��+��G�����n���;� �}��PZ�2{�����ej���c���3+���Sb������]��B�I�]E6������=�M�������|)���{�-�Y�U�L��������V�*]����dD<���V��b�[@L��V�����]��g(����4�sF��I�t����+y��zm�N_t���I�9>������3��y�M�IN���BGY�	n����am]���V����>��"j����l����j�X���k�e.���qm��^	����%�������V�p�8��]������`��p����-�^����]���l���Z��������D>(_����n�g��(�����b��`��������k��Pa�7�95�h\�) v��c���;��vso�5�q��_b����&/�J�O(_����,b��i�J���������Z���������4�4����j�����zk��Y���C�0��[��Gh�_m���o�����(�95n��a&����1p�+�9�����Y�b����K�����K�������'9O���t�G���4�U�f���I�yU��t���J�����gw������M����YO�GFU�1.z�����UY:ae�0��#�6S��o�u��x�Z���\�d*��2���{���R�����Z�6����������K.��L����e��C�H2v�����WqOV���o3[�*2����g�*�w;/R��T����b��2vg${����b���T"��_��K�Q���v;��
��{gkj��,���Y��7�w�.��{�G�H)G �j�\0�S�3�P��g��
�^��t�r�:Zmspb�'rD\T�^��C���0�LQI��n���~���K�0���������f�ws����7���8N^��#�k*C^�^�k.����p�1���BW�d"�+��o�6��j�S{1��Q�kZ��+;��IE+��M����d����:�w{yC���w�*�a���s+��I&�U���n]m��x[u��/�}E����!���������#�e:?w�������8973i������=���k������pDb�����C�GY��z�N�����Me="j.�m��t�Z�S���I��*�������';WB��G}^��U����>�~�)�K-�o���QL'���H��O@�j�P���U9b���)�(���Y�l�\�|v������]�5�/k��x|Y6�,��u�U<����`��S�S����:��Ny��r��$�V�"=:�+Q/#���j@'w���'h���h5����K���=��Y���u�+$�����n�����{�����V���X��;^@����t;�.N�/e��:��oN�U-W,�4���=	���{�4��	q�)�oz�^����/(|�������O���]�������������!�w��V��z�;:{����u!�7y��j��z:W�$���U{3�j����$g��@�ZE@�6�}���+�G�����[������YT�R����$��q&g���K�;t!�>7
c���K�$`��e��g��*�8���
����p���^p�X-T����N��I6���Q�PqK%F^C�dU�5����.Py����]�'���m�U������������9���xzJ�/����<���,I1V4	���0`8��A�����z`�����Tq��V�s�an���)a���5RCiC�T�V�Ll�1B�f�o7:��r�����y��b��i�Kh��Q�eB������s�1rv���{y�t��vn�7���1^89�-o)�i���7[�v�W���y9<�����VY��g��7����TB���'tU��_&p1{���P�R���=y�L�Y�Jt�1^g	\�~z���O`����y@�.���[&���$�L�S:���{"Y�O1����������fS<g �u������1
|�o&e����S���|��u�3,wVL
��9lb������f��;n�fh�U���k���bt���|�#i�4X�/��7��A�a����u�^�f>�+}��C���\�����Nd��c-<���|�"����H��j�.��Vu��i1�9�$Iv��y)����� ���1n��S�3�)A��[�ys3��W-���8�m�g��Vu���N���������f?6������FG)���%7��R���F��j����Q(�3��Fr�����P�
p##��Mr�
����:�d���5g/mp������{�la�c�*&b�����;����
Es���	��j$�t����s��a���u�L'[�7"!��*�W"�L�*��1�E(=r���i���=��}>�n-:�8����.c���r�[xe��D��=3:�������
��c8`�fg�A�OA�����;Q�<��Id>�}���BgY�e5q�q�n�����w�r�f��T��N\9�0�{�����������l��v��z��b1�J�Oa�u^��Z����b0��]*���4��IWXC���A�;��W{iy��������\�����#�w������\*�r*�p�������V��Z	�a�y�����(��V��t���G=w��h��"�Q��y�����E��mF�����G���[�r����k�D�����R��=�r�mO+���~\�I��c�\��0�m�+h�Hg%d&����y��|�(�x/e�}��x���z�����j�������1�"[��f�J����MYZ[���� ���mG{���f'����2b�1���(Ud>x�y�Z �}��{Ex�����(���>���}���S~m��*����3Y��������o��T����QO(��m�3X�
����6�Q\���~���U����+3��z����$�{vy�Bts�'%����r�wJ����b5�Y�����<�F_�����
	����^�������&f�?$�R�C���S�.����;����[G9�3��*{fY������J
��Z�����_>a^��yl�	������]U	����4�!]~��4�3�}Iq���G��!�!z��9��(�������tv}����i]t��%:���,5���q��;+���9��{����]�g�
o�����B9�8�Z������mP}��%wL��i�(��������3�A�i8�F���N=�vc�����\!u��p�����R6�*{�2Wd*������S<'*������~h��eG]y��%��X�Ex��6J��=��~�c0Y���itDn)Z3W_�B��[v��F�p{5Uy�B:u�Cu�>H���i���=q��R����-��������/Xk�^u�3����*����h�������d����J�����{]���(�s����N�ot��hQK��.+�F��9S�4F[4��2�E�P���v��u��/
NX[�K�9lY�>�:���������Xt��D8��I���,���m��kz���C��:�herBe�&Q78�EX�J�Z�<�ws�"J�����7+����,�WK���~��/�"�'����������/��
���P�TM����(buKet����:_n�������+�LHs�\8���nlg����WG)T�����K��	��T��b�;�7=J�����v���7s���N��d^���W�Q�>�-jmt�M-���T��������B'D����=��`���(
�u����N��m�/n�(��6��^���Ij��f�S������&���e��:y�����K�[Tr��($wu
���Z�u}���
&�.�6SR-��s����lWO��#�#��[��.�������C����Q�z^�1�����J��"�������1/��\�kVo�<���cCF�du+xR�����0-���>��8�^�'K�u���Nm�[:{^b�\�S��+�}4��D*9�%.����9��^7[Z����,�hU����F�"��x��O]����,`��`�fU��c2�����9{��of9��|�e=��-K��U��m0�W�\����lm��=S|�!��>���F�����Z��R���9�.����^�2�����6���4�p�#��&tD��/�y\t�S�g3!S��}�O+Q�*�������Rm�����y^�r�]3qo�j}*2����y�]^#�~��&Q��i�1�����!������W��]�=���=	���&������[�&�6N�����Y��>����q:�������]��w{R�������t�J(��6������`�YT��������:�b2�7��A���S�����	��!����Z~���j-g|rSy
�k�a�Z���n�����*}i4!��������H��W1]�._[���r�6-�����`.�$
���ez��g<����N�I6�3t/[M ,N�AQ�:g��
B������P���x��o���!]`l|����,�j@����8)t�xO'e@wk��
���T�^`�{�����~5@���w�:��8G�N4E�"3]RT:���bV���3��K`BW��m<������Fc�Z�W8�TM���y���X��uJ���w!S���R4�UZ3�;S�{\��]��9&���F7��
I������8^�f9�_j�
}y���3����;s����M�cVb[���c��������~'��v�Fu�iw-7���ak�Z���'+Un���n'�Y����L�c%l���{�K-8�����6�8��6W*HL��KYP�k����u���*���N�K�-�N��m�$i��]���F�����'

xe�����aSHs��]tqj�|K�V;`�V�V��E����z��3{"9Y(��%Y��������q�w�����<���_#��An��lJ�M�����*U�""����kn�`�����;����v�I#�K�D�*.�+39��+FKWy��y�y>�U���d2��<�=51��Il/F��7fFm�X�n���f�=��zLy%)=�����|��l�/��
=
}�k^�d2�1L!�����a�0����!�`�/k�8%<��Ha�zi�gkw���SD{O�9�^�E��Z�����J�63;�i}��gF-7{q*<��8��{~1w@;$�G�X���rj���i����"�Dk���������g�@��>}��h/E�w:fx����*��d�������dY�o�����[��_��p�}��:!J���BY����i���](b��3��	���S���i��F�2�Ky^�Y�{�6����) ����(�K�Z��	j]`�le�0��t����&)���U��*�9;.��M��q���F��J�*6��F��m�-Fez�?J.�-v��[��^�����8\��TU8]fM�P�2�n�n��u�u�{(�����`�wS�w�I:��t����+���9(*�������]�O�w�P	�k>��j�����I���u<@������9��������U��h	��z�^u�
c�3Vh4��n����������#��i����������i8����g�l��0Hr7MR��xTWn�����l5{�����`������4x���q�]����w��>v�q���������9{1��3�F�(Js{���f�00�^��}�{����<���A�����I�[�u�P�"RV��Z�.�&U���1<
I�g���-��~��}
���=������e��o;zf�y���;���Ce����
�f��?W�
�>���1�#|%;b��Ggf\��=�z� ��\�F��^_T�����
ouT�w�U�]�='}�n:u��;�XscG5�s�rvVe������\z7��i������=��^��)j���g�r���{�����9�|NI��-:1Zb+hs���UYgq�������RL�Y5��������W�������Y.�	��}�����_0,�y��R��+����U����V�f����*�F���r�����C(M&V'J���S����o]���u
���B���#�7��2�����mk�[p^>�G6��C������iX�1����-_���N�f1�5^z����3���an�+���4�,�M�����s����Y����"�2`"CY���.�.��Z�Q�#��`�A^Q�:��!e�&��-; ��<�Z�{������0Ow^1O�+���aW �}����}0C���2����zd;�r�����W�vS���e��$�����z�S�s�����9���������>��xb�{Fm��x@������&v����<��#eDK���_H�IH��I�o9y�9�=�D������Bk���������K�`�l�QR1�
��	�^�X0���3r�)��RA�u��xN3V*n'��&�{f�sr��qV�z�^h%�r}�z���v��|]z�v����G���F{ZZ��f������Adl�^�{��g�^���u;�6{[�Z<�o�E��������e������-��FRy[q����a#/����2j-�@
�b��tz��\���6U�����2n��������[(��1�����>B�&�\|FBemguM�j���������L��
��4�S�%Qb�*���~'����`�����(�G.���fLmdu�fYw�K�.9@w5�3X�a{^.��7�����2p�uL��j�O\M5x�'�������������Mp��xc��b{��;��W\�����8������s��2�I���e����7�-�s�5�^-��8�
��`]n���7��D�V� �����������:;�J0\�t���=�3�D����]�.n��A]�J7���*]���u�3�^�KU5 ���U��A����*U���S��S2��7VOn��m��%��S�^�����x�Y�R��e�3,Y�e�a����+�J���������R�X{���-��b�U�$$&3%��Q};4���4��D�p��|�.��VK�6/T�o+0m��.�/;��]H�Ro���0Po��(��r��zAx��H�B�$��N2�����^���Z���j^��\�E�����&Uk�/5A����1�l1	6C�L��V���ie�{��:�'ni���I������+{k�����l�;
w�9G
_AC���Z���)�=�)l*��e�����7�x:r�����j�CFc��!6:*2o��J.����w}�Y�'����J�QI�7xWE�M]Z�	[���6��D����lM=���1��9^����On�L����>�	�n����yh��H=����z�����%�LpAk������w�eO��7x�M9�d�D�����6������Fs����O+�����p�����`��M����0��L�xqD�}X��y</�_�;0:��Q7L��|����T��t����4HUw�m�s��3N��U�i^Fmfm�C�E��U�9�:��O�C}�`�FV��oo#��{���-1H�F��cu�����l��,����w2�Vq���y<�/�,���1O\�\-��r���]���B���X���HC�������ok��wY��7��1m����Jf����{���#t4�\�)J���b�"$���W>bL
��Z4	�LY�e��B�������MU��I�8_Esb��d��Kb�<*b�K�
g�b���\^��W=��:�"�����kE��&��&��4��4���nfm3�pw�������^g&\����Oby�?B������-�����r�`�e�)��waI2���������[Y��4T=�U�2WG,q>����)���cc�.�������[��P����x��J�T���Q�������w��{2�m�Rc��^�S&/N�l6�=���
��D�;*e��d�-Y�hkI*=�;0d
��g�a��u�����#�,��N���.f����sp�{���N>�������(v�����b����$�Xn�1��{N���CL��������x�}X]W�c_�^��9���V��{�<'�r8H���g��y��Ps������{u��Q����H��S��}����qN���~�oj��mX��%���1������U��,o=�	]�
���B����o]��w���0,c�������n��jB�Vl� �Js�|��pv���B���D��xm��x������k�0up�'����}a5���������^�6O_T�v�����S���r���v��n���m�	�������4�CQ�B���0�Z
����d���Fl������]���J��������u����
�E&�����}[+#M.���t��'�U������4��K��/q,Y=���<0F�r��%��.����N��a�q2�v��:�b�1j�79b35�[����Ni���w�|y�u.�oox�n/���/Q0iW����p�
��U���)�4���i�X���OM^�;���:�����)=��|�Y���wG�������1��+�};��KV�u�wp�y�� ����\Q=��j�#^x7N�����:w7�	,�qq��^zw'�(��������.�8o�5
�i����g^3f� ���5I�\����o,OnXs�4=�cW�9�YW��3P��1:C+����V��Em��)��*^.P)m��ekZ��V�I���U�)��)%Qj��:Q���<�~��
k�}��U'�{f/���90��y4����L���kW�t[�a�d���|SX���g��i=}z��
�*Yu��N�;{\��)x� L���}�F�;����l�a�P"owD����s3GK#�`������3�����&���?��z|}�Q.�[��4KS������'�qi�����fWA;v,��Y����O-�{e[uq�{N:��a��<E����=w�gs<[�� ��;�WazZf�,Qy��=u�x�ou����:��'k���R^�T�
����X���/~4�:�6xp
�U���n����o����S.Y��/
��o��Y�&�����b�A_7@��W\v��M�����{"W;�Ru�B6��@�0��9�K6�nd�FM�f��snu�	���^�aVZ����cZ����y��G���k������k��1��wa�Yy�a�;e%��tW<����TQ�j�.�$��
���<������w[��EA�M���UxfW�"4�zvv��'`������[	�%OX'p$�x���0jw���3w�\J3�v�=����+��s�w��T2$�R;�{C���\�1��w���S��"^����=M�f�07~�<�H��fQo���/7���^��i^g���b��k����Rrv]��:Q4���6ro���N�=^���FA�u2+�Nd���:���������I����7�{�R��������LI]���
,�^������&fx�I�zH
;�ZU�����#Q����^��$��9;�������gU�F���������E�Z1����d����7�d�v�_�7��2a��6������v�t�qol�e���7g�^�P�yx;���}s�[���K�a�^�B�Z�1�'f��8��z�,WnYp����s}�<Q� �XZ_�%����?o�.��	z&���1��jwE#y�OP,������z���k����+n:�����G�n���K8��VtWS���v������-��U\�����=��
������Uu�K�^�v1-;T�$%����n{_�������2�H7.Q
*�!IKc��:�ss<���p.�,�j�E~���������.��m<hs����3Y���N���[\���r�4������>z*�?w`���[����s9����!Y
�W��ym[�o]wb���&�[�j�������X6I�+�5�5G0J����������5i�N���ht*o��Y-@��Z��v��Y3]�����j5�B50�n���A�8���b���{�;�
7��!.��wj���sY�	
���E(�F� �<��f��a�
���]�]��q5�����D��F�Rv�g��d������*��v3N�c���b��T�q�r�]�}������Xx��`����Mf'��,@LJ78<�x��BR�D��{��N��v���y�g������o�x��X��_Y�l+�u	R���m����_�w��
8�:�7�{j��������7��q�'Rf����^��&�����w�����_m�v��O
��5o�W�^�]�oH��'����W(Gb�8��k)�8M��Z���6��u&|�.�wk%�ul������q�_�w�5zS�K|5�(��������M*N�YS��j�_[��N�%m�gv��&���DVw�,H������������V�Y�v:����G�����-�U�K��1�[�t��[�Q������No�G.�V���*�����d�i�u�f�))�=�jf�c����\n�x��Q�PU1,vS=���� �wDc��I�	��0��3����#������K��({Y�D��*z�
�{��d����i����et��}�mwQ��1�+��h��t��M��5��C��=j+7���x7fMn>���:�+8[!��*3������h�kz2e^��!v2j������y��*�Z��&�5)s��7�S�{"5��ON�����`�3��Fv���� ���)@v��t=PA�n��6�I��Q�N^���<���eE������QB9Y�R�Z�s����J��f-�r�j8�J�i��S�mZS|�g��c���Q�m�]�W+��it�-_8����"f�����t(��R�>IN�&
Ns��ewm�B�2��B����v�u�����e�i5q:�!;�8Eb���3;��NT]��;|�eF}���:���|������5��b�*O:[n��F�wR�����~��;H-���-3/�����IT�.;C��O�}�e�g_tU�r���b�^���RR�r�/�������������b�@�v3C��q�1�U���i,��s�����(K}��ei�pL���0}���8��������}~��S�[$T���L:���X��F)��Z��P���N����c�;�x�+��62���D�L��z�D����H��~K����6��jc��h�]��?��J�e��a��L��P!��Z�P��c��"#��l`��E��t�����_&��=b��YC^>�
L����E��	�`��M`S7��#��PGi��x��U�&lK[��@��AM�G�]�5�����W�������U��(�S
�YC��]46,�����l%�c�d�L�����k���I�h/:ZZMMG1�������y�Z�����b��m�f�&6�k(j�i�nd��2)����p��6-�`��u�$�����/�V �P�)���@u�����;���&���������7D����Q��e��k�	�0����}0������p���^��E,���p�d���[E�3�N`~Z8���X=F�����y��� y�jn�-^�i��,[7�r�8U0�/t8.o����X6���32�����r�����i�<�[�7�1��p7�um���EtN��D�w��]a���=Oiol�DL������leAL�����G��=�������)����Q�xF���S��^5�c�Z�<o��^�uO�nN�����{a���Gyn����:��z�W}�������(0{�jJRou������=o�Q���5��&��o�L7X�]]�=��+�V�=s��R���mU�\s��a;k3����g�Vb��rI���J�#���W�M����R�E����d{�~��&�
����1pb;���A�eQ"�s���y8-k�Wv+P���"yc�`��x���D��"v��z��7T������H�R������W��C[���oe+r�r���9�E���W}��+q�C���a���1�v�_��Y/3O���Y��������J�+�|U6��lI�q�=cO=���k'nM�����gH"�|cz|�����f<�D��9�g�.�=����U�Y�T#g���R�c��*@�!�eS��m�]�s;����������0��r��qN�;��Z����$s�<5��,P��� �(cWc+�r1W�m��VJ<:�"
�8
�'�;nx\�(��J�����L��32�^B�f�d�Q��
.�8#t�������s�v��rd,[�%E]�%�+���:x�w�-y�?1�B-��G��z�K��y�P��
��h�\Iyf],��{O�Xld�Ve���0q�F)}��m<�l�C6}=9�i�v� m2�T���^����
�������|���4]1�p+-I��P���I[��b�xq��b���E����K�:U��m�WmjG����->p��o),dMbL3�[��h�c�����h�NE��z�'�TVP��NSL�u��'0X��sa�G�B.O4�9�0R���p��LL���Xr3G���WA��G`��D#:��Z\��*���a��I�X,��*U�w�e
��j��qe�A����Bk��G��h�������r���G��_�24������r����`�6��e���C�7�>uD�����[=b5>�Pr�j��[�w�{P��
��}���08�D�pxW����yq���5�e�S�F�W�j�	�zP��^��:�vv��k�3"�����&3����Q|��e�Y��n'�����<�y�������1������Tj������S�������7N�h�}�)���b��1L�K�W6��![�(�������
�2m=+��.9A�b+�8�r���`�������s
��v����9��m��-�u{��-�QO�:�������N�H��<���E��A��;�|SY2W�w�m�~�0�;�����3+�.�^/eK���Q5=`�Wb��>���{�>]�����:�.�d;���l9QY�����������%B=7���C��C3�sl��F�����n(�f������n�3���^U2jT���������9FJ�Q�E7��nk��
�B��h�Vu��v�����qg���#�O��c�����dAe��>[Wq�%��l{�+"S�$�9��f�{��>b<����yE����3����}��V�oL^m-���t|�c�~��=�+��x�/X\['#��*��p��>^~��9f�I.v�B;������cy����������0��p������^�����$)�2R�a��t�����/p\j�������%V�D�����Z7��Jo20�����������t��m�N���L��<m�f�;F�4[���dH�d^OS��u@���~��f	2�f��U����J��������\1��^H�F�9w��������z��/���",������A!X_z�+y�![U���&���l���5.[Q:y��u�J�1��F�)����=

�GJi�����u.s����k����E!{���2%������B.�����=n�I���xP�1�e�+�z_�w������M��m���NZ�o��=�h��g�OMg*��y��Ee�|�6��pzY�}�\��s��(D><
��A��i�T��_�����l��m%o��o�?i�[����y(��Qz�w��
����z����]~��'�`��;x���Ij~5Z�;2�~��=����w�A��3}�u:���������$T�N��d"�TF�lv^SR���m3Sa��;b���1��Lr?R�'[���\�*�toR�z�%�Z2fV�xeC��q��B�&t@72��*�'&�k����Fg;�oi^W%�����9sNza�p�T�]�4[#5����:�����ZU�I����j^���D�hn���;1�<����C�>r��0"i��5k��_��6��;VEz)&���|�-��=k��C��x���d�{����������w��5����v�������b5��%f���8nc�}���{��#�5C���K���Om��y#��M�/��Et��c�7Ia��t=�����(�}��M"����z<�m����������0�Fx�/}��b���`�'��1sK�eU�=�o���wv&��hg��vA;�y*�wb�'��&V������F�="��t�A}�'U��E������7�''�nB�KL�����Vp}n��2���1�)a���������QFpg���s����o�C�J����Ou��I��XG>�����S{{'�,U-�%u�2nF���f�G>�
���7vq^����������y���3��O�� `����9����j����������e&���]_�"0�v��l��k��y�R�*r��k|�o���Hi����*��Y�hE��0k�u�fE1PWdSwue������Dv1S��N�w:�1W�a�����%��FuOb�+���Sv����$���]�(��QS��k9��A\�:������,:hu��Q�:5�������v��.�Ig6�k���yJ���kq���q���F0�#2��m[�x����������x��h[\+����Wf����-+
9
/K��;`�!�+J*�'���XI��� ��p�r��sK^��������9��P�[fox8���}���*���%v����FF;����X�
7�-�>�A�����>��6z�Z��mf�����Er�#a���W
�����6�sSa�����W�r�}������u[|4�Z��z��a�b���n;�N���2��s�{��
3�7���G��ku��c�I�@��g4x���I=�����{t���w����[�i��A��n���P�,u]��Y�����]�������D��gb5�����rY���-5�-���9z���w����
x���w`����}���]=|Y���F��f>�uK�{
��.�]S���'�E0��|j�c����/�v�/���K�k��F��Wt�8�)�y�h�3��s�C��^6����^��$Vp*���d���Ju?U���V����Fk��1P��Y�Q� ����|s\����m��]��r��X7��S�
4[G��U-rFU�"�p���p�2�(��+�^���M���4Z���j������Vm���%���<����k�U<7I�Dw+j�e���}K�+�z�7���;f+����40�>�����_
�0�.�vt�!���xQ\�;����^Az�7[Fs�q"=���y��wZZ7��
�r��a^;�x��AtB�	iUV��g'�W*����4�f��O,qcxZ�~�N�U�z�S[6^�3����*S�^�L<�9�:���B��:�}��^+gl#k���{WZ�31r��$�`����l�G';���<m*s��!�X�v�[���f�W�_NFYs3�HO^j�����d�G��h|������Y��RM��4�U�������[�W���V��"�Pz5�|�o�@�_�	.���
�9Jab��*q�}�t�=���]�\�l�J�+nv�i�e��
�p+��uDR�K��/�N��4Y�N������Ss;��eX��T����
���^}�9�lC�eAy39=�2�,U��Y�[NM����X���������� ?R�|F��-����[}�~�J���%�������$�w�k��'���������@��)���l�^[g����������P�*�x�����u��k�+�_���v��v�F�����)m�������g��~���"�>�<��&x��0��k��2��S*���i�U-u<�\�M���/�S�O��C33h�Y3+nm�p��Y-<������mU��s���4���]1"��<]�W���������7�Gq��no��'�{�������������
��R��n������S1[*��x�m�SU���qj�xn��6�.R����1�-��C:EU�9����w��s^;������+������,���p�����7��H�Tj-���a#��Zf��%1���������}��^�drZ3}��:t��'mi����Y#�aX��V��x%Cy��:�p����>d�o�faG�|3��M<�#���?o����"���;�='D\�-nb�%�����N,������;
 GJ��f��k(fV����+�e�H��w�����`�[v��[8?]��t�J�M9�,��\������/��h�^��u'�����-��g+�X;{����u����c��]Yi����U��=��xU��~�+n@Q�/uc��o�w�{Q�F���R^�<�V�0�d���~�{1�g_������s��A14�^������'
��\|6�
���t��U�W�����V���1��#�l��d�;R�u�@b�M�[�(������b\-����/,��vC:�#�>�Ab�v�����0=�}3But6��E����
~������'�^�%7�.7D��P�xR��[�Z��|�G=��=\\S(>�^*��z���x���L��f��=��'X���%ej��|M��L�c&�`�iK��q�~�^�8zl��j��X����	�����/T���!���B�&�s5ikXN��x'h�9��5���I������n�[�L:�����d��[�M�k�u���`��w���vq��lP���dN\�g��c��U���O��H���T%��<��IM�=$���T�w$���:5ux���n���������j\89���Wlz������H$.6�k��&CV��P��Z	eT
�!��o���G\|���*�x�r�^{���W��Ms�5�LV,��������Y�6��.��so�V���{����)(v����(���i��|M��;z��%��E���6�Iw��,�p�P �g��6�T
��&���.zb�d���]��d�<�p���a���_"�~4aW'{��eC����v'[������:F������N>����q{�^s�J{��jU����8�
=���Kb�co��g�>&���
�w�xZ��=]cU���,����1��A=n�x
3�Z���n�&�����D�4x1{;�{��oE��C���������Z;���w�����R��i�,c.g(�\��
0r]�]l\�fK:���+w<�C��!�&�%j��=������%W�=TY����!��Y�����i�:�s� �Q@�vus&)O]`���p����������vbF�6�e���q� �zpqni���]�����e��tPz�,5�����
��on��������=�[��.��zc�WSf�O��uO|���'j����������'�J���c�U��1w����A����_!6���[�Z�K�P�p�V�H)��j�y����*Z��.�<�r���~�}mr��Uq������3�R��v��=��J���K���"��y�'nq���2z����b��LE��3Gc%�����+=Z7i{����D8fT����������'d�]��8��*V�� �yn������K��������[-�K�O'}��a}�A����6N���c%�����b���K<��p7��n�B"�t���[@l�[�`��W��U���;�l��6^��=O:vMj����������r:�;�/��K���M�N�������Z)?�f�"BF�������������=w�r�d�T'I��3�[;ECK�Z7�v.z�������"U���{�JA����5���H�A����Q9���;���=I
Gv�>����_�s�`"�d������}*e�`�WI�*��l�'vf��g<N ���.\�7��u
��v/!�n��eI	����H*D����������0�\x��V#��������
�1 E���e�|�*Y��y;B��d��l�����"�n�<�ni��~+����S�3�y5Z�<���7����+�|���Iy�&��w�Z;�j�p��M_��I��k���p��Nn>V���Kh�q�|���k��{��2(�QK�zD������	M��5d�S������Pm��j��d����gY�}�6R

����i�eX�4������GY��]�X�Up����.����HH�"�w���"ge7�(���V����lH�2�����<�����<.a#\����������}}�
;���P���v�Us���et�.��P�[�����I��p�8�V�z����nr����{}��8�7�We�Z�J��x6�����N��a�O�=t5�*�ok�!�����L�_@x���nZ�^�3�w9�X$� �]'!��s6��3�^w���Z&-d�k��9���4�y�o�L_V�
�[U������>}�vi
+77&D��������yVR��le�>B�vb�M�jQ,\��r�u�":���*��'5t�_R�)u�D��0����.�%������x�`�1�m����h�� ��F�=��|T{�����V8`�s&���a�>X�����PWS�'����������c���O|-a
t+��H:W7�j���r�hl���,����=3��<
	����N�1���=;�WX�2�4���[`��[r�B6*��c�^�
^����^;�H��U��2��	Q������`%-�y#��M1��>G�r��C$knZ�p�Sc���c��b�K�����q<�d�Q)z�6�h[B�:��c�h���6j�nw0�*�������s:�\�3;{����n&��ZNj�����^�������OV��,d^Vh��3\v���:�K��X�]�R���v�9��
\�-�u�Nu�������i�Q^EGFs��r8��WE�:
y5�������.�z^������8�C1�t���v����(7^uX�Zk�q��oq�v9��Y�aEs��l���HC���vM#FOpe����N���.��X������s1�D�[����mb6$�z�C��Ue��K��6��<��X����R0"��T�y	:I��!���:+VW\6�,�����U�b��^�;�-���F	y����Q�f�N���gA�����T�����]N�;�����p��#w|���������I������L��3���;�������)���"�{=*5��`��x���lS�vO6�y5�@}}��|�i/�n-����B�{I��v��(��g���yt)���E^����/=���;�d��]�6B������y9�j�!2�t~0����?�����<k�^WK�����W51�G���/�!��hJ�:��s��C�c����DYs�scl��Cox1y�1���.��f��;c��y�~wp��f<���x_��~�pm(�3��d��W�X���0��Y�a����`�PnK������&K���=���/E��e��$�N��.X�X�u�7�V�mf��d���N/���~��j���~���;���5�'��(�1�N�Uw�����yL�RM��_7����D�[p6���u���*�cx����K$����9;��Np�)�b3euY�����+��gao����yu8��H����&g$j�c�;���6��.�Mz4m��:pH}MBcn;mD�8� ���.E�,�w�9{0��Y�u�����
�zB�E��rw�����.�O1i�I;M�~k�]I�M�������B���.pW{�-�}S/c�Wexkaf.s�z��w&5����J&����~���������|��
���O�Weu�r���;�D�U��NP���wDa���z�RT����[�\�����O�
�q�w�K6�v.{�:
�+pI�l�,�\`��}�jL����A�(��Mf&+�<������[Uo(b���+��#�t��:��ke��r��E�5W��������������Sa���>|���j�T�M
��IV�*$C��%�����v�+���2�)f�=q���	���Y�����j4�O
^#E���{�sR���|��U���2��'��;���@���U������~�5P�=��U]���X�FW7��t�:�V����Q�'�h���f���>���<�/ZL���V&m�.1�<W�v����B�Y���U'\J�o�#���c�E�x���n�u�Ufs���59���w-.����W�X��3�h��j3���)X�qr����.���^���}A�FP���I���]#��Gl�����BYV'���LU�I����3u�7��3�)�f=�K�.Eg���X�A��u��\9��s�o���8��v��-����Mj'e�1yo�[��My��l�9�G��1*n�����nx�*D#�-#�����[��������
�����a
�+�a�z��7�i�."�����)������3+�F�Z�u#��������L@�9����"m`�������9�u��k���^s���h�o�����tS�]	�c=�{5����E�K^�Fd�t�=Q#��b��=�#����\��"�����i�w�����Y�w�!�Z�Ec(�Y=�;F�}xg�qt���3W�)��H��{��x�H���^Vn6q�����L���x���;�o�Ho�/��Us���\S�O�]0%+��%����'l���K��������[����
������N�10d����Kcs��ZG���k�f�W�#�������d�?/$�${�>l^w��Ul����)�x&���1��a=]���n����"zm�����%N��zbf�����#��Q�{�/D	�	/L���"K#C��"`$�1�b����q��1	p�-T��k�`���}�$s,��O7����][4��LN9#�,K�s^�iS�
�F��_L��5����;��������V(w0c���EP�Y��1�����e���hnv�^=��*�Oi�����r�A�Sr�3��t��h���y:����E(��/��Hs��,�[��t�i��8E�73.�J`T�h����I"��^�g}�v����&������1���J6���O*��{G"�!>�����g��m����!r���@�j�iv;��a�����#������2^��Jg��?'zF�6�����;aE��O�q��rz/�8�Ev�K�yPvg��& �K�l��V���v�Cqq��h�$NT�O�oO��30g�3���u1�mS^����4�7V�h�n�t�S����U�u��+���^��5��?
:�Q�z���7�.{�pvp�f������AQC�$�v�#��k�L�g��Wn���|��
��^�=]����l[>�Q���N�Rnq���A��a�E5��)JV�w�EbO�8m����9�3R�������������v'�p�^�zq��Ol+k"��O���^U���U����xb>����6^��G���y���R��Bv�����N�Xv�`Z���1��mA��o�g[������0dJ����i�g��B�kBWl����n�d�b��9�fe�����1
6�L��\�a�L���G6�{>A+��UX^��s=���|W����uXa
!U���r�gza������kV
Xm,��L�j
�xL\dhnHE��N#rf�����t�w�������X+�y�+�L��Q}{/L��k������*7��g}<8�j��zo�z:��v��oo�#��|���J��p"�F�!��E��^>],����yq�1���oaP��T����^�p\��8os����l�H)���s���P9;S[�1��`]"�L��]�9.�.�����w�v�r��k��S[&T[�����D��0�!L,�1��F�ii����[�j�XG�VC����S���,u6��}��$��+^�X���Y~����so���=�3r5�Wm����:���~j�������>�/a���^P��T(F���Q=u4��(�h~�;-����w,T��J�l��chcG%y���=�F�#�|�,��G���z`��H����j����_3���x��>����
u�%~e!��)���b'��O�(L]w_r�������)���j)���@�m
j������k��p�2&����360��5�Z2a�!<yq�c���1�N\�.������!�T:(�(���M�{�t�}���/\xoVO/:1&���G�t[�G���!��%�az!�����{U)>A��l�RM��Q~��d���5���Z�Z�����PF��/���.�������������I�r`.��%�A�n�����B������9���y�=�u.[+�$�������]��e������3���!kjV���X=3d���u{nP~fp���n��R,j��*�.�~���s��}IL�+[X�9�eJ�������K����~�z���?4u�3|�;�^B�b����X7�U�y~���i��,�f�o���F�0���V�Y4���	:\{�1�r��8�Xfep���]�q�f��'�!1��`z{�oxCn����=Rk_�e��~��[Uc���G�����;���RB��~�U������Uc���=��kj�Q����������W
��Z)��:��k6gWM�V���wO7�o�{U�4�C��W�Mf<ql����XB�����I����VbL<9o�������w�|'��d^�n��<�-"}N�1�n����^`�u���
+�h�x��=*>������R�t�
�������.��<7����</D&�,���5�:nx���w�C�e�����,���+}2�����
1��z�FG��i�*��K/�i-�b//Y�%�e�hC��i�\��uVn�_	�H{+����2����n��]/8����{vr8a��V���!{*�����~�-@�&L��m������ �^������_�V*��=tY�������}TZ��:��Y�$����z�%��0�3�J��e��z�����?g9���<���6w����u��v������x��<9x]�s�����~v��Lo��g����}�(#�W����VdU0���7m��7�r�D�����U[��&�2�'}���{�~'���~���	:Y~�������=^P��jjZ�:���5���'���~y�(�-��e�I����G�W�v��9����wh,����;�[�|��U��U��.^{��bC!
��4u�j�������8y,��d��q��G�p���,�d^F���=z�Mb��ki���@U�"�i�
p��z�n��OgZ��;v�m�d�[�g��;?7��y���:U�}}h����hW��a�S��otx7�[�Zi�������I��2���3B�OEf���W7-l�_wt���]����u��p�h��
6/��|zt�;����^H
�1����7Th�+q�ff"��z����[a���H��.���>&L^�%yg^���u�����1:��������m�B�����Fz����E������&b���=��R�E������Q)H�Q��J�u�0�K��w��^����;����{�����Z�@��A������������:�]dYA,�M_/T�8N8
����"���22�n�G��t	C`�m�6��N�y�z{�Q�vM�5�U�A�sj��!$dd�aex�E��SK������}����u6�6��bkw	���#��,��j�����Gk��
����\�\Q[O��=�OL������*MP9���i��h9]a���������s��q`�x�kwz7��m�z�
��^��5��������vWv��+�����]��D����O�JQ�{�����*�e����zp(K��#uq�mo�l�����v�i���x�85B�����]��@�m;��Qj��u7U��a����Fi��v���]�k~1��H���|��3)Q����������N�%���N�8b������-��3��s�,;�8����Tw��l;�HWg��1�iuH"6x�yB��)�|����7��y���;#~�!e�����9wVLV��'v������?
����I���2��\��Ug��}.���+��0d����n�rh��5����}���3-�v���Jb�]��zb*�3�)6��1��.V<6����z��Jp=�:��%.J�����.�6�Z��-�u�j�#�
=�2�u0�����O�����t�������5���h�o���I���<}�����~�Y��o��>Ck|xg���^P��+�{��y�DY��h���Z�,jgI��������/���][Pq��Y�Y�Uk�ZKy��N|�
V�\1ma#:�b���%��c<2����x��v����w�b���e���m'����:�+fh��L��v�6K��Q}g��]�@�v�Y3|���7`z��L�����^�+w���z:�I%��S���o�����s[�"��u��13.�,����m_i[���;����eNm`�7��p%���s��f�z�U���R��,�i%��O
z��y�������D����G���1b;��$W �P;�t+�I�[�q�D�fKwU�Wc�5���o�c���:&5d��;������[\	���A�?�d��^YS��.����u��u.y�>�����3~'���-�U�g�_jI��w/_�w�U"�90]�s����m�q����r�^q��k�'nhY	^�t�h�e�����m�	�����c*`O���bgs����������B������;��1�
1���>��;�+o;�A"�.�[�w(�8\O%A�����������x��c�"�+X��V;F��U4�gv�����z�����H�C����^KP��t�;�*GQ+��ce�H�_�:�^���w#�fh���������&�_�"}�R2�f#��jdIA��o�j��
�N�,}]F�n�e�=����'e�j��S{uh�M���I������>���j����z��g���������l�\q�f=FoSWzb�����jb*��x�i��t*�<�xg�������P(o���~/6V���T�]39���:Zs
���[�K30�4�t�����|~�K������I-G}���k�)#P$2]n��r*����p����Y�������Wt��[�D�%�R���o���IJ�|[�t�|�d�	��aP]���x�������Gg*\���s =����_�T�[#��+,P�}��BV�Uv��`��������U5������vB����
���*p����~t���v#�w1��������������������	���$n���f^L�N�5.�f�c���F%��:���\��w7��cto'd�m'�p�5�%`��u�{!������S�#{�=�98�8-Q�fo�M��������D
�t=&dj�����#����i���y���t�;{SC�o&e����R�K�/�r�]�,8�i�:�[�"ys�7Y��F)��^�m�����S����8��s�'^���j����4Z���nco*��V�vR�NCj}vNF������8b�6J{7�s��Yo3�=�����]���lm�H\>��nS�����?v.b�2�Uw:FEV��;H<$,�+n�9z�t{�S���f�x�y��:�p������$�k{yl��f����V�Nf
�o�����z���b^��q����<�nb^gn�)�u��������eN��+�a�9\m{��	dM���<1���-���(���=�jR�
��M�y+�U��6Exh����fU��!��5���[�\�6|L�������K��sq$?J��*�m:�����<��:U4��q�\SClr��9C5CrCMn��`�F���,���D`��GZ�x��G<kc�i���1p��[���W��g|Xt�fk�������A_*{�ZiK���|fA�t���,�C����i�����o�l;�^UJ����-����u�;���M"�����L�f
�&#X��n�Q���r���n��s��(���FT�DZB|{9S���x�b�����-�xAY�ov�{.c�����S�tr��#��<���p��K�{=�Z������0���Q�+�����3���r0��-�y!���	��;�)�wKDoTI�+���i�W�w�����S��=X��vh����u��<7ca��#����R�����YW�)N\��Xn��Y��\�.P+^
�$����WR�r�C�v�@�tu���;xN3��Z���*L��=�b|��xBzF��Tj0�_6��i����x�cNP$q�"f���n��;���x���o{&��Zja}E�c���q������)L�������${��aLtq�qM�u.�J��mk380��4��XWP"���=/�o�Vg_���G�>��c}�;f�+*�0$O���J��J�w���Im���f���%�j	2�3�oT���C�A���)�����i(�>���}q9?����X�`z�F��%nI�sT�Y���+��qp��.�L��6��c9O���3��A�����&j4 ������</R�%*����vFo���H\�6�w�������U'Z�v�R���9�^5���eu����I5�1r�����	��G�J�����d�+7��K�_z*��I���x�K��#��������|��B�e�/u��V5]�k�k��o�w�����7�����Q",�V>V9j%a�U9%I�sL{���p���r0��I��x��T^�s���d3��F���N�A^�^Y���� ����v�������&��;��z���Z�E{����(������mE7��>/����u��[�e.<�������������s��%�`�2�/�
���a���2VK��v�:�W�0c��,W��0G��.���W�{�����cxM��
��U����Y�_yqY2�(�zE���8�������qsa>��3:���<K�
V4I��0G��{��1\����V���Y������V�m�NZ"��y��<�6mv��5��,�dUC��f<J�9����sX<bt�;�{C��
+^#���q��=���]��+����:���3�T�wEn��PKKuh ���8�����;���"��b�n+S�����h)5��#�{�����S�u�fY���<}����)#�S��8u�S�'�����M?r/��nx7���lN�����ePyuQ������������<�ei�"����W�����P�Z3s/|Fkd�|��}&�U1���G��{�'���:�.]�h����,�����9]H����7��6��l��3��v����V9�����Q��=$�n1�!����4\�d);��������g��g6B�</w�_f����H|4����������^���mJ���|/}�U����t��ej~o�<3�9
�����S�b���;������J���
�Sn�����N��\=��w`��������.��#������0�qx�z��G���*��et��n�Y��Q���'����"������I���}�cq��)�3���`$���	��%lQ����v.��y���^��n�=��FL��m�W���Q�|�(T�F\E
��N]SLNJ�y<�`�*9�toM,�O���,�e�0�4��U�p�Od�6��Sq��Yf�J���q������
�,a��]�;U��h�������NW�������q�+�
������9��N���kWG�zf��|����v�����W� �d�P��� ����yH�
�g�6�E�S�}]4����*5s�S�@����jTy��ER�K0x�R\q����k=�*�Q��������p�!2z�"V��a,�4e1��U�e��b{TmX[0�ZV��Mq$��Fxi�M���:]���Z���n���z������k�z�K��x�c�K�/�o�|`���_�H��oA�j:LF���U��2{mb"��W�^�e���k$)T�y��U��s��q��,c��������������m`K��7OK�$�nR*���9_��w����;�����tW_ef��<�"����/���nx�������=O|�HkS"w�����U�)�%�&�_u��<*���x*���N�/����2=~����Z	�^E��2�����V��o������e��4��K�EOX-"^�7N��K�ye�03=�N�B�����n��CQv����Tn�\��E��+zZ&{E.W�O\X��zJE��2���2�MdEC�������Mt-{�\3T2`_%��bh.�#v�M]����m���T�SFAnM��b#���xp��<6$[��W���g[�T����<��dVg���p��pw��N��L���-Z�oF�v��|��7s���N�q��wux~�������B�����2��y�s_��)b���m����w*M:,����;���f>��F��m{ ��5�&jL����sN>[��,����V��z�zg�������b�����,���R�����tOv��8�3�;��G�+Q^���&gz{�3[+���[���uj;�9���
�quV�Lu��Rv�N;���k6����R���@c����__����R.�h�A9��!�������7�����MwxuQ~�i�|�J��rP���E���
�����y��N�\�;eI�Lq"2�$X�4�3��X��|���3jp��;N�����#7����'pL����zM<M;��ZI�2-m���|F��wBpO���^E�NU4^���;'�^� 2%�}��2�m��`�Rz9�rY�F�����1BZY�fAY��)�T�����+�aG�����J�4-���B��m������ji�D� q�b��d�t�����g �����������g�z_OK�H��i�vF��v{)X=\N���W�����$Hu(}����aW�C���k60���p��kQM|H��hj��35P��������`ux����f_@������\�4���T�r�H/
Z[��9�,;�e�����Ur�),�E���������P86�:eB{��/-i���uI�����rF�������]�*�T7
�vx����[���O0H�R��i���{��Pn76������^�����q���T����c<H��a��26X���yV	��Fc���]"���g*�2i>��`�d����n�m�2v>�H��YD)9���d�HV��w�V��i���J5[����_V��-���fw�
�y�������2~���-�&apF��`H����_=������W���������>��-�~�������5Q�6������_@_c���fh�u���o���;7������]���+_6�7��
����X����[l�V�^}O���w-Y��V�k���B�]�]��A[�Tz���!pZ��\p'��>f/���(8e�N����)�����>��5�=�F����oumm'���1����x���R��FkL��Sb����6���r2����u�5;x�������<��
� �Z�Cy��/"��1�z��
��W;K������oD�ce�"]D��Q1�� ��au"t+p���ef�K[��x���eh5���v���L��,�a��Q���c%k��.]KT|=��k+���1V4vc};�I�N{�)J�{K�{�F�Q����I4��Y�u��������T��s0������_���l��"���;�y@=K�Xm�]���w�
�7|N
�k��S���r������gs5������t���ug��u������*:��uT���
�D�k�������/�{�������i���Y��}W�{���7'K}�O�)y)�]0��������.|���{Wb��6[����}���JR2����^�q���'�{|�����Y�%���\�9�4bFoe�k(�lK��~;v
�X��MU�������:�gy������eE�E4��/�m���{�Z��\����zQ|t��!�-��
CC�������b�`R���y�>��V�wf
�3�&�ot���'��7W<��>�7�)
����d}�L��dyFy�Y��������p�k-_�
�Af�Y�����m���hi���7y�`���
�r��@Z8m������I���XzDf�����;m���-�������y�.�JY�>�;q����U ���|�d�*�4V�����5���M�s��c�w�b��M�,F�,�-nf���,���a{�sg�tz���WR�t�t,���������C�C�1���{1��}�];<�*�qY��|�=���S�������K;cgmLu%�����&6�'_.���w8����4�1�lM,1C��Sz�HC&���r�5x����X�Fl(��q2�^���������b�L��Tc���`k`��grlq�[����uZ���M~*������,�:����y_�DKS��������	�
/��/
�Zc��<��^����:����sf��BM�`�����tv��k.s��.�W��o7[���wwxkWS������f
z�b���-���������t���C1�w0����et���W���Mk�9���������Ky���\����.�]F��%-O:xH|H�����9��(\�5f:��)����
���2/��'���Z�������WeV����(7S#\�:(s>�*���X*�wv�J�&2)o]6v'L���b���!9��"���q���x��?����c���,�7�>���$=��]��/�������&f�t������vs����;-�P���ZE����t>������������s�}�4����u\��}���5j�=��T��I���B���`������s;���	����n�BJ�n�=��eZ}�3F1������\(��M�r�������t�]����X��`��0��r�5i����}�]o7�S;��S"F��go��o�e��7.�����c���������}a)J�c��:��{�$���V���i�J�Jv��uy��'+�zK<o���Y�q��6��d�LJeZ���y��g�_E��\
N�F��Dyi{����.�� N��.���6��y!��V���9T��V3��[1m(
Bp���;!�N�h
��c�q�+yr���H#B�u�����g���=k�}��{��b��G���S_�yx���x�3����3���4b�|�z����*e�>L�R����q��9��zR'UB#R�����.�9�$�k�]��
�-\�u���c��ic���S@xr�7|"����V������yV
p�:��	�)k������=��J��C��5=���j��<���_��`�]���'��Y~Fk:-�{���{�4�������l��]���������ZPe�����W�K^3>X�V���3�i+3��Ft�����@��1�w|,B�mF��W	���'���<���%b����hU_�v���0���AF�R����5)���\���������Uo����B2���]X�VN-�p����*+o(����{\#�:�X�o�1p+�A�������[��v�5��}�l�j�w�����Y���\�3G��]�E��42���oW�=��9��������'$���Y��+�	�g���(C��T���	I� h�a���R���Gw���vqs}|�W�8g=��0�N���t�����?0�K��;�*��3���r�bj	^���e�[s��q�����<��%�2������c���i|%'�����0�X����[�p�Q��`���h+zd���K~���'�^g�n2�i�Wd���� [U<x����������9W;�����"�O��p�C�u�~r��HBt�����U��VYk�#b�l��v���t�g�^N��!���	:#��Zu��-2���7����pz1�*����s���{�&�)��f�2����ww�i:r�g�����nh�������x�!E��	�����u������S	��\9d{"��4��t��z�P7JN����|o��J+2�k���hm�H��)��m�^r�L�������S�*t�<��M���Sz�����@��������MNnP�+��t>�o�-Rm]�VuI<B�7���W���5���}%G��K{�xe�jt2�3v�����1�2���3�����&�)�@uR�$`j�o�����/���w����{hn�
�JWv��
W�q\z��>=�c�9n��A���?�/U�fH�2�����e�u��j-���N�=w;�w�)m�T��{Q�C���<�0s���8o���78�5��Te:����e�S�p�Y[� WMV�7	 ��Uw.�j�&�U�����V�C����!l��aa
q�d�K~;X���-}dk����e��������;3��&g���o�u�r�v`9�_U=�����p�v �~Tz��{2������=�����DM�zjO"w��g�s��U��iV�1��������1kx�sz���k���U	��_��%�����s���LY�y��xf�v����Zl��D(���q������q����+:c���E*D����4�v6.��=��YD�({��	.o��T�T4�&��OTwtB1|�5�����)�c�.xk�g�T�&5��#.U �;��%�8�R3Z2�^_����v}z@�k�Q��r�$-(&^� ��f{4��N���w�
��7����w�x)D+��>����&q����o#�[|�M�Dt��i���$�X�}w�Q�������IM*
�#a��B���]#��:�����d�����[y+������N]�-V�iM��d�L�����G�.]El���,��ZWD�O|�"K��2�oeWU���R����^-]�������������Y�R������x��{�z�� �����9��}7����*�1K+�q
�hK�zZ��V�6uZ�B��C��=�!���C��A�XT��[����d]A�����"����"4+K��{�N���~9�i����`�]LA���P8�<��37����n�V^V�������~�o7a�3uo�x���������gr�%y����?eSW$n�����rMe�[(��B��R�z[
:qZ�]~��w�\^w�Ohx�g����a����vA��fWo����~���i�Y��2��������(����R^7�8{H��F�d�������+j��u�T30�~(Qpq^�8$H�����+��e"k�p�.�t.ez��y�{|�k�d 26oz��d7r_���}-W�;�@������3�o���r�u��c�>����[E�[��L����[w���x��9�&{:�z<�����J���b�S��K1��1h�5L�Y���e�N�������4�
���H�����4*��K�N�o4����[��^p&�<�4�a{�����[x�f��9�4v�9dU����{����Z��'���0��C��s�����/"�wJ9�6��	��b��V����
���L�0�]&�wB��/=��6dem^�����.uS�MU���ib+zxeP�F�����W�ycE/P�u��������;�����W��E(?o^�	�H��u���\����6�<����.����Up��$
����'r��m��>���\�}���G�����fm�����O������.��~�f�Dk��]��M����.����{���x~qny���]�cs����<���t�X'?z!�<�����wL���a<�pe��t����;jx���:r���.2�}k3���,7��Q��4\vJ�+
o$Xv�(ci����i����*be��h���h���Q���*�I5�/n�R�rq��*�\� ���l��B��E��b��Sw\7}�&�t��<$u$�s2���S1e�7���D�������+����#��DL��i��s!�����7����55��r��pJ�^���t`��"�S�c�P��v��z_!���k=mX���|W_����dk����X7��KXHOzW��n.�
uK�
�u�r�����r0U�J#�=���A�=O���{�g
]��Q���
G�M-���)���V�����Q]�mRX��p�5|&�o�eR����N
c]�!K�]�=�U.�h�N��?C^�IZu)x�/%�s^
�7c��mF��3�,m.�
y8njj
�+�o��Y�sn���M�����dR]��p���'�`�Z�x_*o:�qz��k�c���f���{+��y����=8^\��|
;�����i\W�e��x�m5������n��~�k'��L�$�n2k5D���/������������a�{6�d���s��d�8F�c��������5wL��}�o�W���M�S���~E,L�l�S��XO���tl���G�M�G����w+��;Xz���&I9��z�w:�bB��z�*�G(�(5=��'����l���-r��NN�����+g��>��D��9�.��w�|�:�V��\��	�����b"�VC���F3�v�u��z�"P��5���]-������Q�����I�T�[�+�"��W+U��� n�zB6fg���pay�WE�\��]��L�W=L<��yy���f=F��or�o����@�nr�^�'���n�vk�j;��A�z��}��%�q2}��/h�y���}~1�Y���oEp�����Ro-����Vj���8 2pIU�a�knwn��Y��-����.��6	Z�3���`�79��.��cx���pj����PY5����Y���ju��*���r^7W�nQ��z6�6|"Cr�����w^
�Z��������d� F%b�����y�uM�9��m�l2�&8-Ks(rx�����gn�f���"5^u���4u)����I��-�[�R�d��!%f��Ko�=W�'���o2t�[�w��7���s*�n���imh[�q""Ua���mt�yz:��
M�z�|a���Z��2��T��a�����	��pYX�
d���4�����������������8]a�r��8�!���vu�^���M

�E`S�'��v�'����u�`C�*���7���^Xs��q[��&OV��*v�yx��[��X��`� +�7	�wF���Zb����?>&�m�{)���y{�z'��<Q��\��^S���Gl�������z:��k�3�hu�XN3��
8�v�Ox�}
����uJ���/��9<�]�7���[���P���a��}���!�;�����}�I���#tS�w��-�����u�]#���{; ����/��][�T�8h�;|� �DW��*N�W����DnC��:Z=Xw�O�+�=huNn`Q���8��o��iB�]�\n�=c�{��Y4.`�ge�����)&�j���y����ev��J���>U�\i����k5,�a��:yR��U_w^f���gWv�N���C����� H�U�W�P,�T�?D�.SVV�������MW* j�D{�[����9n�|�����N�4<�{k����H��>����.{B����i-�;`�KX�R�fxy�����C=�v���q��{F���Sza�r��p�r�bg��Yt3k%}�}z)����r���:�M���^��G�ul�[^k���z�I�^]F��6QB����G�������������}��{t�dO^�+��a$�����3K�M��8�����%T\��:����$���a<���~C�
�������%���&�3W'�l'ZMW3{L���R������%��������;�)���^pf���YW���j%&�F�O�l��=���?Sb^�f���������g�X��m$}�s�a�
zv�V�������� �*��7g��O�������8
�����b��������;��~�lWX��/o�yK�wb�M�^���1+!��|l�.�g_��`ghh���|M����d�|�{G�L���
v�����9�"V�q/z��^��ES ��S���:���>=I��n�t3S���s��U�V�s\B���&��EJjDp���
uO��g����,����6�EY���~��_A��eU�e�����G}n8�P���as����x�������'�� �l^��u�&��-�O��P'Gb�����x��;x���*�iX�]{������ye�
��[�?vb9^FC�����
�sT���������A���0!����
�s����6��/����_e
;���J��+����jr^O7����Wxx��/)���$��Rt�*���2�Km�=�#h�w2*�E���(,��W���5���c2����)�
�J��z����Y���zN3�R����ro��u��`0�v���M6��Jp��2��Bm�7]�S���Uc�P�)��as�Kj�.�/,c�Y[��L(����T����z���:�ojz2����J��2��[����\�)�W�~T�2Jq�e�g�����im���W��xQ�KYuo���^���S�RX�-��2�����������RS�0c�O�dAf	�s�8Wvu=��620jb���yH�"�*��������.�E����H�������Ww����v����r�l����'f�;z�E��M������{w�DD�;f����r7[�\��Z�A
��z�E�V[q([7�%����(c�lt���	����g^>
P��Lva�1/���8		��;\G;3*�.K2�w�qJ�K*z�:Q�]����x������m$�R��b�hT�+��Y
���m�{�be��m[�6��7�]u$Gv2�SxFL���Ir��[���������)�<�Ex��Z��y����_G����h�v���M��7��#S�.}0Mm�ZW��F����uu���E�����R���A�n^^��i��<[S�n�����+������������������u�9�3�^��cz��D���rO�������{(f:�q)���u����t9I�
�p�<�/2>����)��D=��y�����5�|_^���B��,��B���GoF�S�8��$^������5�"��{��z�X���=���^(��������Ic6m���a�y����{��.r����Lu��L�c*���7s;j���9 �����1���^��0X�M���[�nI�1:d��=�Wzx�����MT�=��u��K1�m-�U����=����~{�NO���%����s�g��>�������T���y����r������N�������/y.���O���	�9KH&��W~�4s�vw�\��*E�*s�zX��UK.�S}�i�'g�Y���
N-�+�9m���8�O,��
c:�}�m[%�fqW�,�U����j7����H>4��3o�L��n�fZ�x8V��X333����`3��i\��T��]*�
'��/>�PX����et�[��v\�}��7�S{<�>�*�*��8=^�0������]��s=�)�^g1����<G�q���t�uR��@���y�]TOt�|k8a"����&�������:�T[���������1:o{{y�A�)6�
���_k��fj���=�u,�/�����1���,+���Vgg��+J�k_C�����Bjy��&��[�qs�\3�,\�yZ��Na�	lV�~�8��������/WsP�4I.����[���GTDZ�9����v5Y���|v�����4����zw���;hO������[I'=[��r:Y��QC~p�I���M����&We\h�����]���nw�I�?�����7#���O����g����G�����\�S{����`��6���j����������!��b�eGa�����%`Q`�u{<��I�~G�+�SvU�����y�O�V�S:�#u�z�wq��y��g=����_,o�D����}%��������V����)�%�Y<��Qf�D>�f�y**�k�E����=����e>Q�����������o�R��u^���7����`����X�V���M9(9�z�5���_�m��C$~�~y�,��t�FA9mH�f�6k<E����3dSF����/^I���y��nfwV�D�x����)�����*�N+���q
�X��Lfkn�cF������'���h��@j�H(���l���y�x���Q[Lv`�P*e�n&�+D������o\���e�Z�"���U�C$"P���;G}�ud��f��6X�SN�g1n
e9�*�s�{�������I��j��<�K7~iWN���'�L��rb��P�J�/QfP�Zi�'pRm�-�}���X���=��H��"#=�)�]������g����!�'j\yB�zs�:RI��`���Y����L8����++���Sy���,i���.��O5�]t���A�.��~����g���
�~oY�u~����4H��\=*jc���er]�*y�G^�P�>��v�9����S������������h�M�^]���Y6�+>�)[~�����y[.]o?R7�q��~��M��>��C��mH�wB�p��J��pym8	���NmS�{�����%X����KqLD��H�;��E�v4t:x���(���i����>D4V)�������?�[���P�������x��b$7��2��s��1��
�'��=DW��g2�;�/z���T���z���X�<�qP�BS��F����1zz��G}$���f\\����U����q�)O��<m�i������R���M���~�:!*��:��^�R�@���R��O�����\�� 0�Q���5�b)���������40�}'��l{�����N���s������9B�=�W����r[W"��j�
a�V)����	n���Rc�U�����e����.1�Hw����xD��������^������g1�b�-N��w2$��W�k:�!�;�v�nK����7\�_���H�Q��<0L��}T��/h�������Y��5�ju��$59�O�-�]%���N�r�Z))�cd�4v&��S��^Uh�ex��h)��=J�X�=�wy���V����1��%t�Y�����U��O\(���=F�KB���%��	r���jxr�Owx\�
n-�#�7��k;����Uxl�T��J�����d7,Z#7,�y\E���.���G����(Si���3w���)���c�Y8��].k���iwd��Dee^�0�6���}�s�1��Zd��{=��v���z�B�s2WR��=�'�O��������	�����2��Q���9���X[�T&���uu�4�x��7��� �O��I�E/���F]N��Y���B��{�v�k������T�g�p]\
CKU�]e�^o�J(s���V`�$%�A{�)�Q�����k�^]�J�^p���'��m���g�
��3��2�5Is���J�!�A��zg��H!����@��J[�/�1�w����c���m��,���E��}����c}v�vV!pIz��X��p����'���w]R����>K$�F�U��x�l�yMk���O�:�,f�:�_5+KI4;����(�t3]^a�2�.J���,����o��f��9�W]��q%E��X�KV|���C��9�\����3����������t�'��<��`���$�^�r8����H�{�L9��n^�i�	�~����(�Dr�<���:Zz�o����0��U�������T��v�����&L�{5�
i
�������}��b�Gs�9J��q��r��rt��
�3�����U��~������c�]�������n\�������[�qb�7��6�����W�{4�G�#�ht��(���p�V�R�S�f�p����o+"��(`�x'~�x+���=�]R�&���)��*�^�tci%�}�!cH��z������W"�v^����T��T#w'3Wl3���U�����a�6��u��Y���{"f�3�Tn]����^HK{�����GV�p��g��k���� �i]�e�/�*��u������n�C=�P
�B/��v�=j�^��W��E
�o-�5��U���@��;�Y�����x�;����R�S�&�MV8����<�tT��>�b/`�������5h���!9d#����f[�'F�u.{g�q�&�VSvx��fgfm5am
y�������d�ms\��5J�J�o�����f������#�<"3{�%�.y�U�h��H��hub������$�=��vI�L``�u�z#���g�����C�P������T�q9��6�Q�<���{���*>/`�'�1�f��,����2
��3�_`/\w�[e��]*s�������*=��e�W���3[�cC��58>�k�56��k�]�uy���������� ����sV�u���%�:fg��.�4Q�zK���W��e�����~��	������k\
3V�`��6=QT[����t]]�58�qLWNC���2�N^7a��j4{�<��2�IJ��k�j�6[:���;�)v������4E�]����x�����+H�Q5�<�'g�d~�^�?l5R{�P��7}���.�Z.��
���RZ{���g�d��J��_[�z�2�����=|���d��Ba��=�ZY�\��{{P�r����
�f�9��}����z����&x���a���N��yw��i�~��^.?V���W���.�3�r2�
nHU��>^�Bu���9�4�%�"�
�kl�Q.A���h�A����n#����������Oz��;�Z����&�t4u���f��~�0����{@����gt���m��Q<��p����yn�M��;}��M�t�Pz�{�)
��x�U=����j���.����0����)�f��=�%5���6K�q�X�6�qQG,��.1p=%8)�|
e(�9U{X�p��b�X;gXf�M�� 8�1�^���xW=DFG1\N��Z��9���U�k��{}���2���k	n�~D�}5f'��Z�V�L�'���i���������^A��i�*����Ag%Q��>O���
:��5�d�i�7�e��'���sk��n�����.0���Q��/��T*_��%:�t�<��������o#fiF��kV+svwV'�|M��zhs���J��p$��;2�]�=W�bA=���i�n9��9tz��aM����1<a�Q�r�a9�u��Uf��+=�f�a��!�Y���$�G����yt���-z���},52���o�u
�W�U��|���3����
��]���f�vK&w����\1��3+�so+�v����2�XtAz�J���'.�m��d�:��d��R�a�:9)D�����U�KT^�U�_(����Ww�hL4<�tT��v8/�;������zfzr�N@�`^���`dA{���� �=����yg���T�Zu��L=���$��;�S|�s�S�Nvt��H��o`ff !���W��z�di�b��V�������B������4�"��j=���wm2�����*7]���[��eS;z�^�jc
5X�M����7�-l������w:"���r�Wk�&j���H�E��x���-9�Z�Q�;t��3<��S+__�`!�$mX�w���L���W(e;��M��
gB���r6�!4�r9;|d+G�g����{�E��f����f�^����9�P=������`RP\k�J���l��U�n�J�fz��m�A�g�(��6`������f����t@W���%cR����W5��(������q��jUR+TQ��-6�t����xb���Fy�

��p���K���g<un1��.����p�����N�L�Q�4�Ds�3����(f+����66��;Z����OI3�G��Z5��Qzz��5/f*��M��C�\������3��q��������p{��������
����l�:8���IS�'�+fdUq���G�Y����"*��mUB�w�/n������k�l>,��3y��.u_����B�`��~9�=kN��C��{�r�������A�WH�=��Bqj�j�^���s�
�L_u����9q���4i��T_�W���/�nW�O�(��������1��<�-������$/{�����=��W�co��4��[��U�1��]�7�(���VN��N����-�^m�������
����%��{����D_y�����=������=��k�;o���c����o�������A��6F��n��]��u�G2�|��:T��J�;Ks�����EVOv%Y0��9�qf1Oi����.�7����C�$/���:��{@s*j~i��!����_����iQ���!e��Fn�OE��S;J*�����qX�wm�s�8�E�6�4�������WQ�|s{���0<�j�g/y��]w�N#�xGa�Y���b��^����Qi�Kr���W��|�qN2`fZ�����:>ZE����
��W����
0U�2����gQ���[�+���6�c��w{)�2&vxqtt�^>��s<��W�����I(����>�,���klU`s%�8����
��;����H{���p�l/���X~��t�M0s�qw�Y������^^�.���o�2�^��x��\�L���y�P���������u6C�^|l{�J�1���$�A��W>:^����xR�x4��.��S9FM�q��9�����T������wK;.0��iUe��	���',?j�O=JW�e�{�x�����x�t�y^�V�����Y`�i�=�=a�[=��+H���w��.#;nR��&�87�Pu�(���3s�j,�����_�/e�.+��w�*`%wg�3}���vV)�,������*��U�������-XQJ�Ph��$��������)�������V���r�a9����=��j�]�� �Y:[�����;�^h����Ey�]��]�zr��Xl;�:�"�R����a��4U;
�8��c����7,toW*�y[2�P�29�Wx�3fR��;��=l7��U��9��/.�:����d7���"�!�"�\��	b���fy
��"���c8Wu�r�@�j����z�����=�����N?��������P�Q�^W���6s�����z[�����*l��{����Z7S�`�a�����.�Ou����`��I}|�R������f+��:�{���
���� �Y�.��Tzz-������������Pc�m�����x��j])C]���i^�y��-��T:�@T��ff��q�<`U�������=C��S�����1�����i��>��n���(��n[-�m)��hA�l��U�{bP��K^Uw��syX�q���F\��nb�u&��-,{�a��f%���W]H>�v��VJV�(6����v�!�`K��~���M��M�0�����^�;�����=j�Z?f�������&�u��T�Em3���]J0J�9B�I�������qO
�'���t�����c�p��){�o�-[#[�*�w�XO�����������T��&5r��������%a��s���G
�%��D�����:������n&L��d�RVA�,��r�]ku������"�|':)GK
�:����d����s\ca���scx��g���wjO0�vb�XI����������u6|
^�
�G8sf�P ��l�2�����v���
������(��?^>������(<�U ��%��s���w#�X���Gs_n1G�	��%T3+-=��j
���D�E�i��:\�mK�m�r�@������o�R���6�n�|w]���!��#���H�!WGV��t�/�������{k���)�n�������I����!y�~�H���2�!1�G�J����W���8{�m��Kv������Z���9	{m�'����Q@# ��T�iY��b���g0]�����<������^{��s[���f��R���oJ�:O7�nz��K�G�������W6<-�t�`���~��pJ~����������frV�s/6�6�S��6��2�$���m=7���i��=����g�%Y�h�2�.o���q���t�s����mF�q>=��C��0�p�Z>e����~we�U:S�Y�<��u��'F*�O���t�y�������7��������ti`�?Y��g=��Ti�s$��@�����
��KUo�"�eJ�w�F�#���6o�5����OMHh���Bf���N�bs�J���=�����U]��
<�+
�B��S�����j�t��^���G}��*��u1�2�*XS�{������D�����y��L��^G�Iri���&���N��.����WC���/��,�����4����O]CS�3^y����#��]�_z6Kr��l�A��!3X�s7�I?k�h>���.�4�Y������*��'�RU����IQ�,������Y�/y:����o0�}egF�����4u�:}U](����<���T���JX��|�t��^n8��\�3�"�R���O0�������om)���l���2�&����3U�=�D���q�B��PM&��J��Ud�]+d�\��p�0����}�����/��1�����fT�����T��G��+;��+������3�#�e,:�21�5G��#��KUB]/$�����x���:�I>c0�T�'gt}��\����/n��6���d�n��3��9���+j_������h�����L����WAHuI�vO������<�Va�Y���xP'8��}����;uR��=8�L����nj�AvC�1\��o�OuT*����N�	���#0����K-x�z��fV{G+�%mkt:�SM�r��_�	��U��CM,���N���7W�3��u]���&6?;��=�e�Z�{���p��(|�����nx�}o����wxf��GfQ�k^�\�2c'�]*�S�S�c|�	���N���Q������B��>����JP�^�����]����3�\���%��;�;~��z�&bv�z�^�k}�'x���TW;��-��w����EN��WO:~����]<�;�g�'�Z���[9f����Ge��s|��%E+~L��k���������������P��->����
w��&��[�?7�_E�.�X��r�TV�Eu'����9F��k���^���0�lu��G�o�����yd��~�<��~x{��(�R�)gL�r��w*�k�1�<������r����E�{����2K~
����8����M���W^7;����1by�l�7V��t�s�Y�n��1����5��in,��-nqP1MF�z�|)B%�����\�r-O`�t������+�k��])W�S��,����(���+$[��Y����!�]�@������EVpn�pVR�4�QR'wJ��p1���R����z�jZ}�X����F��VY����+B���p��g��(����oq�<�T�{j!�y�H����3�d��t������*y�$���dP�����z-��au��m���>��x���z�����G�����A�w[
b��c/t\��x�U��������/�T5�`��j���M���`X���v���{�|�� �����������`�����L�+p^k9��r�/��/U^:�@#�����2�+5T��Xpz��y��i�FV_+�Q�*e-��.��M���P���F�r�B����w�V�� ������J���}f\`�}j�/�z"���7X�Q�X}{�����x�z�
Qwk����%9����7��:�'��oq����1�z�&��;���+oSn��{��b;3���Qs���>��
Xj�>����R�o���%m
]P_1y�,�t���3���Er�F2�'EX���^_N�e�%4�����q�Szm��;�j�\�y�]���I����t�mk�\G\w����MR�+xs��+���.1I�M*��1N�X�m�p:�m�,�W;���6��L����Q�����EA�v��d,Vt�l��5�o�f��G���
��9^�8^�u��n�2���q�+���q��iT��=������3=dg%.���g�Po���x�G�������bb�!���9x�b��i��C=*~�_O��ug.����C|ym|.��.T����� n�E.�]����'���[D�2'*�3�vp�2��t>�Q��/��v����~��(1���Q���^
�f���Mi���N_?kC_�<����kS&���+���1i�4��z	{�.��W�f�)��D����sIu;y/D����p7��
�#�.=�]\jd��"$3���;{{��9���{�'�]������z���p6u�����0j&���9)t�����}��=H�F�c
.���A�����iQ��2n�fp�].��.��D���~�����S�~�`���N������P�������Z��z^b6-��{�P�y^�C'Q��7�1��
Hv%���L��N%ab��R������:��x��b��n�~>���a�c�3�M�%��J����e�W�E��C���,�:U>4���oH��3.Y���YY�)�_�f2�n{���,�K�����������N�9�|����=��U�&a=p*���!��vb�{\�2�_�>�����O~���U2���c�s�����~��j�r
tw�������|}4m���Yav��J���[����pz��vm?��\���GB|��Oa��<O8������_3f�/w�����f���q�j��.����E��Y��JW5v�����~[���S���:KB>H�or���e`�^� ��2y��s����SNm����������^�v��;��Su�vz��S����uI�\}��{w����28�A�B�����A�v��I�^���pI��n:�sL�JY��W�vZt8���I'z��^��O���f�fq��J<������;X����5=�VD/�yxx�5�r��,�M��2{:�
�h�<����sd��>���p&�q�6J�o�N'�j�J�����8����yeek�������{VI�H�/h�Q<�nc�+h�;*�I]UR�x�n�]%�Ds�/_��8�'Y�u��r(#�G%�0���m����m�x�yb�C����i^��	�c	����}��@�jQ����,}��OS[�v?r���437n5��(D�s��b!��Ymew�J����S���t���.�����IE��p1�\�\���B��l;�����J��!
��=z������!�W=��>��t���NX��T7��W����|��+p�:B��h���v�����~��eUN���������z�S'LsS��Oe	���7�M������
�h����(J*����������mU��s_^-)�iRcr�������4����(��E��"���;C(T2w[���p}��A�<\>}@��������������x���T3��)u�����`���
��}m���*�Z��{83ni�
��7��x�([����E5��b�{O�����6}�1VW�8v�d�^o����'[�|Y��Yz�SLW��}��D���w]|���(�#�\M�d��l���v4R�7
r�jC�A���L��������`7k�"Jzn�rJ�ux������'K���3=��
^��KU�W�$�p^���d���/c�{�"�
��^=A���d����N|�c������3��4���2PN���}I�
%�Nb�YC+e��iu�E���u1q��q�����c=�
]������
z22��:�rj���1
�6�8����]����Lp��C���	��V�\9���F��������.�W��>��EB&���jck(�)DD����
P*�{���n������@� �X��;�1")IM��q���D��7���:y��)��9����z��k����@��:J5�C�,�B'9�cn��9]|���v��f�to�;���������(�e%��C;2��ph�+&�g\6N8����c������O���F�n�R�����w�H�Bn�s����U
S�-�[�"hPD�\:�TEEV{�L���H����N����I��hn���\X�zT�EB�" ���9��x���u�&7�P�����M���<I�\(|����OL����������<%2��De�A��6��J����.���v�������+��o�s�n��PTF^[���U����J!�\�3�9��	
TF{sx����
)��l�����$%)=}���H(�	C��W��7�c�A�A ��������R�D!V�������������y��F�*�NN�A��3�:��Kj��PF�]��G���v7��_g^� �W�/�rwe�2����I\�vq[�$��M!M��T�?��z_WB$k^�{�������EA7�����N������L�H�Wo�k9�e�dDHE
k�vn�"P������u7 	�O�u�P�[�c6���i+��j�S������k������9���=�rN�QJ���g,�U�1a7�V#�\���x�u�����_
�
MD�t������$�H��I&��g��U���o:w�p����}�O.#��yxu��20^>w�S�^)DE(�n���B%"�v�u���5HV/���7������@$�����B�.6����YWiV�N�^,������u �u��
u��������S� |+d��A�ToX_k������U�f���?O6��W]�pJz�n��>$�A$�& �"��ASx��
g\�%b*��&w���s��W��AA��0}w������e����(�I�F3zL���(�	��[w���T ��f��;O_��<�����gn/�����F��Y}��)f�gD�=S`
�[��&_N��E>���1�p�>���_M�v�G]�B�(����%"�\}���bk�On�<���{k��j���B'��{W�On���
!G^��;QHNK���� �Gf.�
c��*��@*�c�{}�(�b{�7���9��^�n���|��Q����[����A'��G�x�6����7R�{{�����������a���Q�l��L��������3ze��M};�/0�;�qX����jM��Y��}���9EP�gy�����@AMc�����q�HEB_R�m�MT)H�9/W���7T(H"�k��=��D�*+�v������X�!�^u���������"E 7�����Uf���Q�������j1Kk;�����2\&j���������/-V����]6��o(+Q�j�#�t�a�]�[��(�anL�`�������+:�y���wxo������������)"G3���o�V�Q Fs�{�8���"1��s���N�b�#T���������V��&��\P�(i#��N���u�2H/0��M��^����,��U���nc|E��A2�YU�b���4�#*�
�1�4���/&����Z��D��v�5�h����j)����@�m�3d������B���qj��)��{z���V������������F0Co����$�P]�\�=y��-@�"!7�|\������dEW)�p����5 ������f=�$�
@ AdF�!I�����U�"���h�{��{�T�)f����@!o��V�Sy;G��r:���.����E��
6������/�]�6VhF��g�G� c[gc�L��)	�b��c�{��(�'��g��g+}����b���x�%������1��=�����"���w	�n�H�QT��[}�3( �Cy�n��e����0��5y��]���p�_Gnd�+����S������:�\��1�E����W������s%_M�������2�h�f��_'R�F�n�KN�@^:�G��{<���Og9�,HRTV���k��/�Q TQ���Z�&�z��D!1�k��n�T"N^�u50�@zj��]�W!) O^�;��WT�H"D@=�{?����wg�
$W������OP���hW&�1T����3M�oUp�++�UV��!V�l��b�
�~���aWf"��la
+����� �w��k��j��Q�;5�oy�9TR7/��z���$	�U��]�(�Uf��t�yf���C�7��}�W����u7�b�B�! ����I4��v ���-��8l��UB�c5�����l�g]Y�	��`�b��7v�}���}�xyw������VDQ�/5���r����/9���=�^=�r��J
Q
A A��Z�������"l�������@�(�J
���;��'��7Tf
�.���	��A �@�DT���k��3m�����sz����z��^u��8�;'�"�?~ ������ ]tw9�h��Z����{�juH�"@R�@��]���{��j���F�Q������QH�DQ!H_/:���ws����w�b��M��H�R!H�H����=����r����Y�>$���WA�U41��������.������lm��{�H����XU���]>XAQ�����d]3��W���z���s�#rj����}���o��1`�������PA=�sY����S!!��������� "����������Y"(����<�w���s��/4��(����z������) �D��x�X����b�y��9�%��w��^��T �"�AH��=����t��I*W���q���� 		�����D�`���u���&��z��@���$�������R�^_7Z41G����D�BQOw���m5�s[�1��������y^o<v���Dc'�|V��y�����k�1��F=!A�D$���^�[����|�������k��y��>��,fm@���?F�Fw+K[7�o��y��n��D@"F��v���o6�q���U�Y��s���s5j(	 DP��>����*�w����UrCh������N��v�6����y���&
k����!]�/9*�,U�������*A��/.�}�N+uc�t�������gYV)J@�AD���ox���Ro��W����jQ Dgy���������x������o�s�f$g���}X� AD����w��QJ��^>�9�f�b�#C3��nf�c�=4�b�"���mh>������D�&�n��mo�B
BDA"�w���-;���{����u�{!TRB4����jC<f6�>UX7�|2��R���z^��1�w��w=��/��9���o������B�"!�!��T0=�����c�jZ�s����B��o��gf���o�9�{�b7q�B�	?()
�9*�$[����hU��1pR�"D�R*"g����;u�^�-�M���C�k�T�I��#��~�)�w9�'�����o����q6����gq�zz�$�H ��w��q�T��W����z��E�.�#��B����]��'{J�����n�v]��+�&���I�kN������R��me�6�K����������c��B��������z�`��_�}�r��(��R��o2�WE$9������~��&��V/�	T���Z�Ub�T!Ro���Q������J)D�)	S��vv����]� ��e=�$A$���H�1�����P��=��o0�M�A����]sn�	��[L�v�#��k4������P� "BDM^9=�g�������sN��������O���;X�O���7*l�����J�
�B$D��4��v{=�{�����o��g^�e�n��WH(H����F*�J��|����Z=��>?~ ��$$��s�CB���k{M������� �����@�e��y��pt��Y���T�
�M�x��w��^�yo}�V��F=��P5���u�T�$@�V���{�.�����'������W����
���%�;��#kOg>��cZ9���|�e�m������� ��������;;T �����9���pD�Qi�{����iA���5Q!PA3�be�s��� �!g��{��#EFy����n���7�c�����P �!P

Hcx�b^��r��^�{�����_6�7x�wYk<�1��J!@�� �I����(>�MEn�+��h^���<�!!H�
"�s�2MY��V���S�$�0������|B�)(�3;�On��k��9.�\�u������{W�k�� B)B�������G���}j�Q��l��NgHB)(�����]g����g;�7y�{����w]f&���$(���?^�i*Y�3e�b�����=�����"���E�9[2On����?,���ek�U�|m��C�QV
��ws�����7z_=o^��/�����H��R�5r��;0�cG�5+&����������-���,7Y������M���P���,h���|gb�;(��.���
��R���K�L=�y�J-$> ;�e���4���+����[|^d�D���&sl����B�M'���9�dANn�1�g�}�q�$"BB"��z���DH�������9��BE��gWn^���p�3�*+E�93P�,3�w��.�*^g|	O�~$�	$A�;$j�%����9����Ov��Wt)�P$"{o�� ����vCaf�S]�,�3������(��I<a�{�6�����2zM����U�)-��!�@�$@I����u��*���3�k�����R�"�s���s�K�����>�����'f#���$�?*�����{�ox����u���������z�D�"EQ;����������3�{���n
�����L%�3~���:��k���.�:fs���DQ���^��=���|���+w�.N\`�W�K�Er������s�����cx2�ov(F�n����X�^u�+����}%�Dg�7_i��M�B��P
"��w�9�{X�9�8U��CA�EB*�{z�!D)8����=�
R���������0�����K�����DQ"���{���r*v$T!�zy3�������$R	"C
���a#:�&u�7g^:{���)�@?H �@ �����y[�u��+��N�����|��f"�D��������A9+O��v�:j��[@UA(����5�����;����1����b���"R$H9����hfs���{q�;�$M���AI��=�������}��G���>����7��|����Q�*����;�q@���wf�\�������""E#%���<�g����_;�b{������8��)(A'+/�9����Oh6�WIo@o\�!��P"	k�I)wr��Iu��^(�)s��z�C�1�8����tLx�)����k�m�L�t
�L��f��2s/��c��Ok`��r�j<���]�:�($o]����� ��1�{���x� �!B+oo���o�����!]c���.a�	^7�k��n{WvJP�w�c���$"D�|��l�H�������oi��/�|\���)
"DRE#Y���g�����1�sY�������[��4" ��HP�hn@�7}�- rMryW�R�����RRP$P�N�������o��e.��^oo<��X@���~ �*����$�uj��&�����I�V""��(s�o~>���f����o����fy�3HR� B ����eX'���EN����'��"�E(�RD�3~������;~�q������������JH�@B'�	������mo9��U��hI ��RB��7���y�==�L_X�vk~����/��7{�v���![���g9��RE2����<���<��}w��m}�����I�%������qUlS�O;=�!����z��#��}���8he���\�G��X6���MK^�x��m�~��lV�DDQB����s��J
TEV�Y�Z��[�5"�E(�
��s[0��D@����)�sN���}�k��
AB����ssb��B
%!(D��]�1����@�g�z���g�{k���@	B*n��b`�(`���Q����c�5%�
��J��\��>o_��n+:7U�}*r����YT[���{^��|%Y��@u<�LU
���T�f�\�^�F5j�#F�7�m���u�I��P���W�j��"��������O�\����|:�g�Z�y��"���=/��������b��Z�]�
��w+�g�������H ���Na��-������NW�D��d&$���Wm`�On����UJ�ry�S
�
8�������u+hMA�����V���a���:�%�]LT��&Ts��ue4��
��jr"2�U�ms��Mut1Z�P��l-�P[U���Y��:|qeb=�]�e����$
�z�I)�VE�/�-w����Z������iK5� ��`R�dC�S*�j��; �L��d���8�$G���5������]��twN	��W�N ;�X���q:�x�a�q)	�U�a�i��sJ{�������J�\�]l�0l{Yr���;38Z���5W�z�\]���Ff�ZN�(�(�d�z�=�����1���m�ofV�������.�"�-��71�1b��b�2���pLMul�"����l����(�����U8b��Z�F��������GO<B��Hj�U�S��a�8�+�����R0�j�wWe�����Sf��srQ��)���v3_
�0�Y�t��|�L|Z�����3(Z�K�t���5����iYJ��O9�Q��
�{�Gu-����)@^�cU�i���)�����R��a����O@#��r
(c���f�%��������L��N���=�]���Y��NILAnZ����C���u"5��W_S��R��\�N������i���r�xW1���a)�����s���y����Y�����v�����b�/�N|,�i���LV�GP�q=���5�-A�c*���ZyN�������rT����T���c��GJ�S����Z���-!lWJ������$�!4;�n�r��;�(��34���x]���q-�����b��5�|�{V��l�\����W]���T%[�����RGdk��Y�������+���������~s5��%�a�%�n+x����Pj+�b ���������w�w:���)�C�o�\�b�J|$�1m�����y�I���w\�	�e"3_Z���V�o
��>O��5�5b9��[�aEv:���<u$��Q���xU�T0J��I%�f�4�m�n=]c�)b�$r�5�f"���M����JtY�{3��J�,/�J���]\�S�i�r�#��Y����(%+U�}%���$�.QW,�6nb��N=
:��S" ��e@rY��Xb�;c��
��;����%����:�����l���wm@����t�A+�n|T���F"TOf���
�0-���.���/�[��8X��8��5����&tI�6�����*�zF��w��I�y���M�uo�\�|d���-4�gU��q*�c�jH���x�37w�I�r�Ta
O1�)�����z�eYoz
��L+],���"�������R���z�lOi���7�3o������6�.Z:��V5q*/dh��D[Y��=��S�p����d��
sOf��(9��f������]�lm���H�q�����0��������y�9M����6��d��N����o#��
�Kz���:��nl9�
nST���q[u��s:m)�������$�,�X��{r�;N28t{vh=�MIV�2������	p\b��Jx��V`iwf����ToT}A�R����x�����������6��Y/c��v�Jwr�5|U�}N��c��������m`Z|��e���)S�� �S&:`en,Q�jg0�Z�x+:�-��bx^X�\�Y��eq
V��s�3JZ�T�����c;�K�A�7o/'kU�A�'d�G8�:5�BtNS�f��+B
sY�X�W$OH)�������g3��v�����_2��tQ���wO�]�,�u���+V�������.��G�"W��7�����c��+�N�wW�<*����F�e���7M��ON�(|�<b�����)N�uVZ�G��H�>wR��{�����j���w����
/��O��:h��������{�yp������>��:�YqP�����5@���[2�{���������ru�>�f�0B,p��;��+�ev��V��
U3�5�<�=S1�jEs�\�pY�h��MgCR����_`������JaZ�Cj\u5���kX���l����vaeT�#�4�����">����h���b���[�H3TS#�+
y	�l$F�y4v#�Y92k[o�%�#��:I}�i��DSGu�t�,f�1]{U�YQw4&�K#8����;�:���U��tD����kd��[v����V�D�P��AN�fM���M�
sp���C]����!��svC��"��v��}{�WJ�n��eN��a��5�	��a��w�J��/�;�wy��r�d3v���sn�f��77jC3v�.��A���9���|�y��1�8r���aV{u��_S��1��q�j�0�����M�����A�i�l�9�H9v��HY$��
���i���z�����8�q��������^PwkYu��w�B�����p���!nm���7�B�c�����RO�U����.Z�uX���x��lf��]i�J�2��<3��{���^HWm����jCv�����!\��5��Cm��S���WgI�>cn]����k���yW�v���9�+���U�|$�$.m�.�Hn�����Asn�\���2G��' �nfJ��O��:���{(r�M'�h�D�d�^+Y�JF*c�nx�\��� ��Cw-��t���!�m�������n�!���s�r�"qz���>"l��7�*�
b����Gie\&c�n�
[�H��G�&��6�
�����Hf�d+�t��m��N8G�#+����u\�U������8
To��V8&��Wo:{����!��H.��
��sv�5����i��!���5��C��{�������3�1��!�s������������������>
)�|���m�[�d7v�v�m��1�{�������/"��+�
B���Bw������%��u�=��\�I�4lx��glz�`o�R4%�����FZ�O�V-�a����j�
�/2���v��Fh�:�
��uA��A�`w�M5q���)�9S�6�fR�M�����v�j�����t�q�������i�X���s�:YSM����������U}��{��7��\/��J�����+|�5M�`]*���_�I�s7��T��������7��T"������On���w���T��5����I�
��3dS5V-�
����w������������ �:��}.}�8%�9�j�y�lW�7M�B�o�c(E,��	u�l����n4S�3%�(Q�������g3�HR������b��*��I,^N���m���7�7�����N��;�����9%��M+���LnC���
���|�������,���kv�
���2�d2��!\��3v�
v�>���X�K;s����_vK}�zNA�kVi�b��.�I���+���u�nf�!���-��t���!�v���8G����|*�����GE�����k������6:M$��U��B�{y��m�����' �)$�����;�d36��B6�C]��1��C�^��i
.�Wf���<�E��[����+N�����D]�����'����l�6�!�v�1�t�e��q�G�4�
��Y���{�/&���������u���uL���%�Je
!�c{�} ��H9�rC3m!�-�3n��rB��Cs6�
�����+��V���x�Zx9�O����Fx��t���z��5J�)�&��>�9#��p��.�C[�Hm��C.m�.���������������zhF��%�u���C&��+��rS���"\N�	����n�n���R��!m�B�[�7-��k �i:g�9}�s�<���(�T���w��"����X��VV�&�uAR������>I>�4�C�n�d��\�Hnf�!r�Hkm��Ni�EkA����S�]s,s"sa�c��e��*�\�q�Q[#�
��m�wjC3-�sn���3v���wjB�l�����=������?	
�����b��^*���8/�&��n�M�[kz��n@>I�>
9>�L�Bf��5�t��l�[�!�-�`��M�:������!���y���Vgr�#��B�S��7��f�c����0E�p"(;�z��+Efj��=�������^�s��t�1.`�3�*K���"��7M�(1�{�5���:O��F�����Ia���7.�6�t�v�:�z���(�y�	X+��bi*^�s6fHpm������_��\w���:�WD+9K������z.�h��}Q�G~|&��Y�Xl�����Uk��+-4���,>y8����g9�����s-�US^hb+z��������A���J�.	e����������U���[{�����On�3��*,@����,����-1�cr���1ln�n�����6�+�vF�1�An�An�#r�*����	����u�
�oq�����R�����������3���s/K�u�UTx���x][j!�B6���Cr�Hnm�m��h9$��|"r}�x�����Xz���������lN�M�V��%na�h�v����c�����jE��) �Em�n� ��He�p�r� ��!����w#�&����>�[�t2����y�k[;t��jQ����|�m��fm�6��������.����S�|*��\\����S���f�����N�U4�2��$�&�S
wn��v�
��A�l���
�� ��G�9'��|8�R��b����:�U�S������#�1^��t1�N5B��J�w���~��C��p��JH>3v�
�����H\�R��6�4�����s������A����*�ud���"WbIU����Ul|��{��f��m�6�n��n��i�>)�k��#��3*���O5���3u��y��e�@������M�����=���!m�����f�Hc�t��n�m����r�!�6�����>c��x�n;���V
��w�]�9����^�wY�IB����}��&���nm��hL�i
�l�����I���� �gL����h`f��9������{��^r�Z����f���l��<9���e�C7-��jAsn����+m�m�n��r� �s���ZA�}��2+C�Zqe`�a+�H�������LPeI�5�
��_P��
��C3-�v���!�-�n�>	G�����@J����v�2-x3,���]f�(l�����;�%��~�y�n��PL�cf��z��n�bc:f�Mg+��>�������@:����s&K�!,�;�W�b.i.�GM�����J��������';(�M!�Pz�Gv^	�QG4c��syUMm8��2�5U�#�|W+�����\��,�9D���WGlY�;2t����X?<��o6r���
�+lY�������J����<f������rVm�5���]��]I�;Z����`��s*�@
;u��k3pR<����'L<���N��U��6M�U����7jgun�P���� ���q��
nY6!��+����uC!o-�b��FD&����eU�����-�)�3���i�� ��[u���K�j���V�-�&	�.�=�i/{;�j�K+{(�L12!��>��$����6��Cm����!�m���R�1��*�*r��q��{xlWf�N��ogs��$���t��W�sk�#���C.�!wm��v�wk!m��&��������A���V�v��r���:=������+�0t�l��K99gq�����P�2�c�L���jD>	�	
�l����1�vCn�C-���y5�6gt3����4pW��ra�*���+ZHkU@��[�e��s����C6�!��H]�����m�-��.F>7�m�4��G*���1]Uv�}�5��X�f)�$�:u)Tun�<�s�|��������!�m!�����$�Hne�A�������^�t�����c��3���������=���'Fk��p �K�>2H>�
�����
����C-����W�����A���l���{����]$�&�gI|����Q�VlKP�7������36���i
sn���d.�B$\�|�}���;�+#���]����t��|z�����*J���s������RS,T��\c�p��M�>	�R�� ���+�vC7m��m!�$��w4�8��W#;�����;��#��6�QV]�g>�d�����H���v�
�n��Rr���m ��>9>�G$�����j�'�
�������%\�wqI��7"mcz�]���/y���t����3.����d76���C-��;��]��>���je�o�}�<�HGk1m�r�[N������w��b���MOj����f��d�� �8^�����K��T��M�����b�^>�u���{m_'S1g�s�c�#�7y]�A������y����Pb�#�������=	���a|��g���W
C%�;4[+_Rm����5a*go��!4a@�D���|����N���Uh�?vh�
p�d��K�B�8p��5u�uwi��gph���J�U��X�H	��T��8qs����WW`cS���l���r�l��0fei�Y�O������7zf�4��c}��7u����WR��� �;�����;j���i�J�W/c�}{�J��nG�|�]M������:���`�?���p"����]��E�[!���U��6�R+�U�����B��"�!	�(lZ��5��C����)�>'�4��sm����sm!��Hn���>��e+�5��=���x�����9���n���������;�����7-��jB������-��]�d.�������V]����h���wu\-�;������\0��]L���#�p����$7-�+�vB��B�m�r���[�|R!�����W�Z�w���\�,�+Gv� ��g

d�2�'�~>O������$76�n�Csm �n��v�7n��������
���|��g����vzsi�1��������	�j�l��z�+�H�#�������7v��l��n�w6��l�2F>%�\]�3~�3���;�J+��7o*(����SJYx�]���2�l���!m��r�!���]��C\����"[�|:�Cu�e'�����j�Ss��|���(eK��s:�HV���w���H;�R2�!��p��n��R�d73n���wjB�k{�2���.n���W��^�z6'�"��Uu����:�)�>����C.�B���r�Hfn��[�!�l��m� ]G��L���b�z�e_L���W���ZI2\��0\���&��|���C76�6���i��W-�[d32�!r�!���}9}y��,V���"9q+�}�njD>�A�80��C���vV��0C���	9��i�vC2�A�� �[�3r�!�8G��"
���v!���W���������o����/��9�r������7_e��z�:��8��L���
�������S&��U9��6���n]�j���zW<����.5G���t���E���@s������\N�l�Y����^��9����nn���G):�������+�.��7��r�q��h-pB���E,\�Un�98�;�2�VK�Ud�(Cye�w9s�Os�j��##�����ve�-��d"����������zE�8�:u|�^�y��d�fm����t�I��9V)���^��T-�����9�y�M`(����E�{3�]Yt%s}���dFs�������\�{��s����
�t���
��P���f�t�vm�p��R����U�|�L��������i�[�c����
k�7���7������5m���S\#r���:��yi���m�+�t�n�!n�Hn�>�F�C�d��������s{�T�r�0q�a(���=z.Vd�q�l�y��w�B�[�2�� ���n[�+��
���.m����;n�w�}��;x��rxr�4�l&
�]���]�/�yU��Xg�j}�m����"��A���-�d����B��O�)��'f�h�{�N����O4]�,.B�.W
��ct�V!������7n����77jAsn����R!�iH��D��|�S����
l ,
���!-����-U����}Zw�������[�Rwk!v�!�m �[��rCv�C3-���@z�oE�>���e������������M�����f+Dx9�����Cr�![�He��fm��t�7c�}�|*��
��v+��������r��	���bX���Qkl�~��o��vCn�C�H\��
�n���9m���p���A�����<����x�;�v��OG�I�������$��:�l��G �#�����n����!�m!��Hk�vC.�C[�H$N/�I�	u�}���N�=�Q���K[�xb���O��R���t��u7��6�
���[�Hnn����!�6�v����n�v�<_�d]�����>�i������Zq,�������-��=:���f�T�A�n����Rr�!���$���|"r��0�
��n7\����N�6���������l�������R��!�����VvJO�O)I�
<���C�U�'7h�;���4vAOK)�Wfv�\3Z�+���n5M�3��v��Wv.��{>X���B�\%>�j^���@�U���vZ��FktG�S�y����%c��m��l��/w���b�\�����L����;x[�-we�����{`��GL�v�b*��m�'\&�u�d
���v;$f�8��{M���N�:�u�����������sH��sv�B����Cd�I�\6f����h=<F�*�\'�;s�����@e���A���
������M�vC��z�({	XG���d;�W���5t�Y�~��r�z��#]{��"�|18�����wS�]d�oT���Yi}�M�(�.DWn�vM�:���Wx_
t3���������i��C��1�vA���;���2G��7'�$���r�����yPU!��!SK�&n\������z�q��t����C������2F>IBe�H]�R]�
�l�n�!��)��
������<y��	��SzQ��H���=������|riwjCr�Hcv��n�!wm!�$C�f��7R����/�u�;B��Z�`���Wt�G��Zw&TP�e}V.� ��Hne�Cn�d7-�76��r}���>��.�"��F�V�ou��O7���]v��Z�}���N�r������im���B��Hm��
��B��Hm��n��!�����<�	�T��Wy��x8��#2r���b�0���GN��7m}��Z��|	��B�n�[n�n��3m��� \���nA��ke�/U��Z8Y�;��P*SqP8���D_n�����/x��>�����He��k������v�3-����6���}������y��v��}i�;��#���sTC���wvn�N������"��n����9�Rr�!��Y�������NO��4UmN�e.������Jw��D�t��fh���9[����k��!�m���R��!������n��5��C-���m�����${}f�+�'ur!6����B�F��r�����s/^����rCr�!�m!�v�m�v�d-����p�n���Rgj�L�)E�������7�b�t����;[h��;�[[�r��C�WR=�8��]vX��|�)��.5N�ao�R�w)q]��#�\�Yb!�gyf�k6K�6M}Xt���6���	�F��d���E�����	���=�8�h\K�R���W�������f�W�@l2�KY*oYt�����WSk����ue�lG��mq�Q�r�3j�5�ny\�F�.�`�5�����YHq�������`�z�B����s�Y-���u�%[�Q������{���Z��������#�
Ke�t���Y�;����M���u���8����^�k�V\{��R��8�_^M��5#���m�f�
���	/.��S��+���w�L�e������9�����8p��A�t�tg��8b�h�gd��$]��8�>��F�0S}�)`�8������d-�H[��
n�!s-�n��n�!��Rv�
�jC���b���l��S�w-Q+f_�o	�!������<�@$`&����}X��Cwv�2�d��r�C7-��O�9�#U�yo�^������}Z�}����46��
����zn�����|n1������'��B��Cn�;��[n���d9�������h�����}c������.���}Z�=w{���Y`���}�i8��'�������r�!r�Hn����|n1�����7��:�:�
�o�yJdO����O��u�B;���V�i����m���6��3m�����m!��H[���t�v�	{���~>���xz�����u+s��7Y��{���;�����>Hf��B�l�r�!n[���6�d+������-���*�����r9A�V���in��4�[8��(ml��GXo3+��c���f��A���3v�
�vCm�����NO�
G���w-�Zv�������[,[���#��;�t5E���W-���+�}�����|R!��J3m!�m���R�k!m�����%�j/e��#���j<QjTo���i�3]7��2�������nA�NmH-����Awn�;��v�HD�#�b�����M+/E������]��f�w{��3�d�,����U�K}�������in�C-��;�d76���C7-���C����n��m�,J��s�e�S�YG.*���U�x��I�����e���-�l���o�dU�}����vr���M�S���#�S�����{Y#�L�%p\1��Rr���2�X �� �A
�*;\r���i���6w��o%N��<��4�������b���OF�w�a��v�7md��Sb���U��w����t��O�eI)��S����p�O��ib�.���w��Ku�e��C�hw��Z`r��,4�^��\�"���+������Mwr5��GU�5���������T����w�T%��#��<���L�e�����W������:����,�u�9T��/z��v!�u����[8_*��EU��p���]<��t�����\���0
��
J�.��]3��������Y��Cg&���`��p'���c�/c�����
�����B��H\�d5��CW#$�|$����������{�S�zT9��M\�+������xw(��yx�f�����>��>G�F�?|.��-��;v�-�R
�R.���L�"���e+���z��������b�;�m��DX��,����q��[��H[�rCwm���H\�r@����NA�����lm���l�z��R��\(q
j� �.CP�7.��~}��
���m����!n�Hnn��m�!�m������Y)�����H�J�%l���r!�]'Sw�hg]���9�7y��
�i�l�wn�]����R�� ��A�jCn��}���&l����\�mMf��b��c��sE�t�>��R���i���� ��n�����v��jB���9��i�\����I�����k�vv��^k��4o�K���
��j��p�6�!]�Hf]��hG6�sb#���nA������U������gp�&�z���(<���I��3|���y����H]��C-�B��!��!�v����d2����C1��T��\�S�*N�����N[X�]T#(��cp1������l���
�l����[m���A���1���� �*�,nr%3tE�v������m��d���k��������9�7m=�~�v���.��
��B��B��H[����9�R���)t�����t�F	]�{�����gv�ngp5��M(%3�c���,:�3�bz�t���SI��ZM�����(�d��-gR��K��O(n�d�[�s��.���Y�"�f��H;D��*�N����L��nr�;��Ie�[7��v�c�=x��r9�HHvr�}�{��H�M����s��pw*�#gh��\��@���S�3�����`����q:f����*��E*V�m��4���b�}ooY�j7�/�dv|��;=�����c�MX��4���QT��d��zW�+:��
�����x������Q��`#8#�N5���9���FQ�{d�v-�	l�������:��5S������+KNnk4���G���/w\b��)^�8�V@�ME�H`�h&\:��b��!�G"ne��-_��j��Rn����-���� ���f[��i�� �[�
�d3v��� �m�	��^����'8�K��z�a��v�V�����N�;�{�r�������
��!m�A�l����7m������!��p��u}����g���A�NWZ�H���g�A�*���5��R���'��}B�*C\�d��n�r�!�m!����$�	�|7r��fv�,��3��������;Q��d�������`����}�vB���n�H-�H9��������m�[d5��Cw�3o���z�����j�nvN����v������Wx*m�,e9�|n�F8����H;��e�p�f[��R����|;L�(�lXOV���O_�LY���Q���U?����f�b�����3n\��m�-�p�wjCr�������c�|���o�����zn�z�*��Oav3,����\X�$T;_0�<��d3v�
��!v�!��!�-��jB��H.m���C�}�w����DN���fD
,����y�����x�����!m�Cv� �l��nw-�[d.���m�2�!��I\�J1��.���GR1m
�72L�B8�1�#����c�Yo��nO�
9dm!s6�
�jC76���!q�G��'�0��l5�}��=�aV����Q����1�vw*��/y�E���-����jB��Hm�rA��A�i�n\�I	1�s��}��sm*��Q�G��3�g���-[������f�!���P�M��W����-T�,�������or�'M~o�F�/\���=��"%�<�~�b�I�!�\�9�����%�^�5�ryE'>��q�`�Q3���D"$S�������E(�o�+���(}�����G@��t��4���}���>���*cC}����TT�/J��S��f��HMR�����T.��C�*���w�7�qz<j���f�����R�ys\�v�'h"$R9����;���mTDH������|�f��Es����Oy������J)5w������d@�7�n�����N� R��c�����'P�)"��qs9]�����%��H�	 HL�z�7x���^���FLY��?6����T���A�'����N$��oV��O���������^Z�<�m��)��i��}�{3t��u�4jI�w3��]��
"{��s{�������@EHk�����_g����J)�y���Zr�$R%=}�~����AJ"��|���1��Yi���,E��F/����L�Q�ngs�����g��8����V��!��.��Q�N/�:�w�����v�v�(�����v����31.d��{�t�5$&�R���dDd�����V����j���do��X|����h_
����q���1���������B �"#�;������R
�9����P�wY��~}��|��@�,{�i����,Y����:"@B@@B��}���*a Q�7w�g��7x��s���Mw��q�o\Ji��.���w�%������}����@�Yh���oIB���yF���@^1�*ST|�\7�c�Y�+��O���P��)���g*	��S��u>���Er���njR!@�~��N^>����D�E"N{���������) ������w��AI}��������S��X�����jbf�+�~��>M
P�j��.Zvh����$F�t�����W���V ��#;8&�f���`I�U��

<����S������[�=�^��T����n��VN
wy)7������7��O|g��Z���"s[G������2���$
�s7��w������F(�~���?{�mQ
5��ys��!!���s�^��UU�/�/������PX�bo�������d,��jLi��W�>���E���O��[�&�Oq�V���[��t�(�F�B��&9�2w�5��/(;���p��wp���Y�JL]c��T,�NC������>��z�3�G����~3��}�_.��&�"R*���~�����p� �����~��,QR
��[���RR$���}���	R���{����b�
B�����~�?^��?^�3�����^i^p0<=tP�5���:7�3X��Q�92���J�Y�Eom����j" Tt���1'���Y���k�����%.����z�5���{�+ �)
	�y�s7�|�@@
�gW��v���R�!��n����=��Wh��#z�6��?{��B$���������P�������U B���Y��5��\"
9w���KaVK��n?/3�%���2��I�'oO}��`I�>�����~�,����7�Vg�X������k�A�|�(�`��>O����|����O�W��E����ME���mJ����������������w�]����	U�}���:"�X'�����b�^������"*�������>��2�)"�����;���R  �>$T}z��C4V*sa�$�0�v��7���o��V��!@�b��6�c������F����S�m
�����-��
+K��N�Y:�Q+�����@�\�?����n������xnNSEox!wU����P���Z�!��B����ji@�\�o]���j"���Y{�&e"�g��3�^�(�w���~�}�p��"$}���K�#=��W^��:4)U���{�=��M��q�11*Pv�Z3��j��8�C�8�G)��/Z�����x��{6;|��������TW2�;����^^��jNT���
5�������c���-|��T
�m������PB$DC����.n�"����������g3�H��"�a��l��B���o�{�y���	(�����"�@R���s8��h(T(�QNz��n��7�f�J���������sS-�%&c���~���="y�]/89O$5��d�����p��w��W�����t�����Q	Fre+K==��*�{�<H$�����_w���8�(�������P!����{��c7+rR9����>��"���D�����9���(+�/���}��JD���s�"����A�/0�K�8u��Yr�m���{����GwH����9��,{[�"�7�`�����D��_
�*�G�q���45��>"��*���"����oY�96�
��n9�fQBP��s��{qQ�O}��:�B�SX��oLT�B!	�w�kI�!	�~������.�-D�
xS���������K��F!F;��"���X8�C�����J��|�&��DREB!=������Nw���������'���@�R(P�TC�����6���vT�L�n�`*(�Qb(�b0<�<6Y�Q���or����V2�L]�:|�HPQR�����=�os��0��4������_��������?��h��k&Vs�#w�2���H ���TD��3�L9�s;���������}�{����(�F#BO�������6��<;�����$H)D��w����~�����k�g�;K�������H!��@$��,��q�;��T�	oZ�d����*��
\�����k�����x}��|F���}X����y�z��B!ffW���X�N[X"�<�^��R���v�:�k;v����$|A�K�R D�\��|�w�*�B���CX���?H#��g����B�D>���:�QAT�����5�����DD�P��}���I�HDP�P�p��Z}����� ������/��97�sz������w"t�tBG��>�P��P� "$�����y���[��~�����w���*l{S:��E�U��
�@Pu�\�kS�f75}���� ��k���6���l)J-�:�^�N�����T�@Q
������o���[W�����g��|���D��E	�~��EU4��|y�dD�+S�,
����&��������d�~n�������s�:��
D* �|�a�3;F,�f]�6�,�e�K�u|�m
�"���D/��a���b��y�8on�T�:����� �	 ~ �ux>�\GFw|����,�����G �/Ms{�%��x�B��X��7��[��SFw�	>R���V�9Cg
�}�3�#�<���x���DTg{q��q�'hP
���sa���$PE$����L}7y�"�*"s[�w/����@D%+/��i����#U<����_zz�3�B��!M]��M���>��E�Pb	F/���{�����
/k���<���r��c���H$��J����w�o����u��{�y��~�1������`(D"R�$�Kv�C�^�	'���v��9�9�[;�Mj��
DT(��Q��b9�����"������4���$���I
�7�����T<�s2�19��w�� �`qy��n����e�^��K����B(� �
(F��M=���s<�x�g��=�yJ��q��w���h�(�U�>�8c�������|��B)B"@D��5i����������k�K��`�:��� ����
@�������zRy���m��M���\��EH����b�/���W����|�x�I�Y�s����*��O_�����g��SX�<�b�����3����q""���z�$��P�'o~���]�d�������33V��PTA���=z�j%o}}�o�XDR�Esa����m�qP���9���OxD�"3����<�&�9�"���@&#���{��������p��i6�c����@������N^{������������������DD$P����[��-��e�<�ks��N�l
$�J�!Rs�����w8�y������X�b�(!@I�1��,�v�����M����K���P�"�����M���T��A��n�l����M���)P���z��p��{���[[���c �� �	$DQ$bg��9�nw���5�c����(y�+H��������+zn_�{��4Q��b�	�Nk<��Z���k�������v�����b���5C���33����9�7��D�����^%�����N*[S|tfc��u��h-�>���-���xx7����}���~I���O5��r� F��{����ADE/��v�A9��~�}��{�7a�Qb�~.�?w��O9��no�����*&���g?i��=��((�A������o3R�*�	H�������s��5�����f��y�~==�����|��QAD�"Msr���rw�N�Y����{��+[����)$J"��;���.�j����J����H�/P���D"�A��[�����]�����u3�w~�+�o����]�$"������c�r��������|�}���|����T"HDT(U�1;�gA���{�P�$$A'��p���m�V5-�U`pf5��y������9��'�����'8<��,�w+6a$D)P� D$��y�y�cX��,jw���[|��������)(B  *�P�(lK�^se�U�RF�g/�G8���g|k�+B9�����6j`�06M�x����Y��x��O�������ZU����en��U�m%/�[_Nz��s�����%���(���w��p"
>���u��z^j�@���}���D�RC��<�/���H�B����`�������"�"	�������� E$��o��r�w[������@���������J��,)M�DFk1����T)E"(w~�g����{�q��k��������kW��;t���T�*��8��7RN6���c0.�-{7��HH��B&����vnl�h���/tg�.����( ����{��s����_}������9���{����DB(R�@>������C��it�Pr��������HR�	��3��W[�Te��S5Jk�	?~$�((��KZ��4!�K3�����>�{�s|��o�"�)DA��sp�]�q�n�S���|�Z�b�HTB")L^{<�>��������'�#���X�\L6+xM����E����B�'=I�/�&�s������=<��-#j�N��ip�����]���R����N�(�<�����^������")#Y�Y��
(,c3K�����0X��s^���D+���f���+�%�m�����f]E	����c�� ���{�w��w�!@�9w8����j�{�����H�PB")"�w��y�����jw�����rbs�X��MHB
(�&�������{{������k�5�E)�T��TQ������������������U(P
P�T�J��{��t�m��	�B��%!j����s/5�e��g��9����}�vn�A�P� �wL0����r�3rwC+{����f��ot"�*!D�����oi<��@���I:8����Q��	����s}��|���'s��7=���s8�����BA��%
�:%cL��YZG�������y� "B �#����@Z��sr�]3���=�F���~V_y��<�n��p�����Q�K������n]^UY|����<��(��b��������}&��r��v_s�&r�!H�QL�q�9���'��Q��7w��	��o�x{�E�_��K_q��J �_}w7��{��\!T����z�RH�������s��w1 ^=x��sZ�S<��!��H�|G�/`�n�g��R�\U��aIDH*)����{��]1��<o]�&9�sSx����JE))��E���e����}����9���*Y�qV" #>��O���{�����������s���$�-�AJ�DE�DHV����]����s����3��k5^� ��B
�#�_=�������^�{�}�m��)")	�$��*��dD�EJ]I��������Q�P(��C�V�Mtu��V��.���	JB"N{zm�ww�c\��gf+Z�����v��Q!��*"
����1j�f�X��N��{[<����/|��*�z�q��ITU�����	����av�����;������!��`���oUO�'��S�z�Orn{���=�'&��
�}}����u{��TAH���|��{�k��U
���D=�<��35���TA �)�>s�:�9$J(��"*5��������}�)P������d{��HDP �D�������^p�DER���O�]s������=��Tvy���Cx�sm��z�{f�x�C(��xl!����/������f���������JC���)��;/�������o�j]u������k*5���`Z�q>�����$�.5,kkV��j��)���zCr
87������0nq�q^����"E���I�A�w��n�R|Mi��R	s.����]���9\&}/xrW3�u�Y�]7�������{ev]wv%���y�ZR�t�)w�p>m{��WU2m46V������E�y]S���Q��48��}5,�P�CM�u���q��9��(�!�7��X��v����x�������\�������q1�`wW]����/�������k�p�,�{��:4x�O�OD�wIm��n�\�_h38�tZy\����M6 ;�Z��$cYXi���v���([���,���~2^��������gc��uE�Y7ZgSU��dD.2�V���6�73��\
#W���W�i����N����i�q���>�N���s��\x����+�zIF�h�u���\�UQ)��*��le`CJje�������������uN�X�^���\��V�\��$;p+�jr���KM���	Vrk�VA���k�s9�op������Ni�cG>��Q:Jj]��|������9X�����"5F1836���}= �yA����X����Q8�R���M���
�����b]`�z7$�]F��U�P�M+�(�k�/3��<
R2�\�*!�x��7�e���iu���I�(6�n�z��}V)w
��0�O^�v]ui���lE81����o�4l'��*�9��Wk{}��iu���6�'0�o�:+�.��/!N�s��9��Lwv=L�9�:Q7�������w�P��v���N�mfg=��T�S����^b_XSf;��^�4e9�j��t�wK��u����@0�i�mS�%�
B�������-�����+��P�Ru�Q�������[K��%]
��MK^c<2q�UV5��\���pZ��K;/�&����rkzL�U�M�	nu�l���yP���������h2���tv���C�N�sDh]6R������t5��Zt��=�
��7��w��4*����Z� q���e�Uk8Q���nV�a�f�M��G����a����c�[-��r�$W/��/o.���)��O�[�2�pet��Zl��������w{C�����u��xv�r@v�^�}��c�����a��i;dL����z.�����q~Z����/1�����vl��R���v����x�n����.�E�;Kb�J��y��j'�����9�K�1����I%<�D����h�gu�x���0)��B
��bAv0�1��pL�_f���>[C�R�s�����{F+��R�{[)�3�X�s����O�A�X=�N+��X��>�� $]C\��X�MI���t�W��o2me*�Cd�+v������E�p5�������P=��H��*�U���"�7�
��;���w��;�y*���
e�
O�;�sk��$G)����5g����d=Y#j��n�E����#��of�j9+{ei��OOd����u�=�����r�Xw���M��;;�"��r�g.�(2���kz]TVGe�Nar���>��r���}F�d�j��5�e^'1���UwS��%[���^����-.��3U2vR���&p1�R��B������J������t����[
����>]�r��GXC�= [$��
0w'u�H� �,�q����h�98�[Sx�N���rlK1=��e���c�[���%E�sI�Y�E3�A����N��:b9u�.������������B�3z�����j��'���_��B��-X�����7+&�Tj���q��;!
U���3��%V8�4���z�������ovd�l�Egy`"�-��h�7�=��i�a��k�P���F�e���������ty�t21��~�i��B�.������$��|�8��{��{�����3�I��IP���HCS���h�����$���r��	57s7
��C�~���:��B,���S���w=�j�����0��q��~�'�$��`@���rx��
By���|�Rs�I1����K�Mg�����LM� �����N2��~����?f,&�+	�pXLd����8��8��$'�%`��I��d��.b�I�����Hg�@�$|��:�>dw>d��w7C����{=�O��)���+�*����?�e���d$���>d�}���~pO.O��<@0&o6d!$�0?$Y�0!��������"�	7�����g����3���)� w;�5T&��I'���<gU������[	]��O��!�2A��I������		;�9��^a��x��$���I�����@.�o�S�bc7p�z�jI��c or�BI'��/2���S��|�I�I(�E�HM�e�'�r?`N�$
Hx�w��q�i�����8���	Y k!��B��z�z�s��<�
�s/Z��nT��ky�����Z��=�ua�"<�i[���e�~G	�cs����U�I������'���\J��
3l�h+U���M�cZ��XdX�T��Y�}r�������Ln�w9}�
���_m��Nbb�)�P�e=�xxxN�a��r�9/Nh�<���[�$9p/��iu�VM��p6=�j�#
!��s������B�C���|���IO�$<5���������,�o���? �c8����w��y~"��C
AH) ���R�;��� ���R
AH)!����gH) ���R
AH){{�� ���R
AH) ����9�
AH) ���R
Cs��e� ���R
AH)!���K�
AH) ���R
C�����
AH) ���R
A��{':AH) ���R
AHw���oH) J��J��J��J�������cY���M�0[���n<<����\�Q,H��*`}���WR{�A~G���$|) ���R���&���R
AH) ������ft��R
AH) �������zAH) ���R
AHv�;� ���R
AH) �7v��zAH) ���R
AH[������R
AH) ������CzAH) ���R
AHs��:G���R
AH) �����9�
AH) ���R
C������>�f�F�vP7����?ee��Y��/(B���g&�������H) ���R�{�7���R
AH) ��//zgH) ���R
AH)
��xAH) ���R
AHg/9�� ���R
AH)!��y�zAH) ���R
AH\�;�^�R
AH) ���R/;�^�R
AH) ���R���s���R
AH) �����Nt��R
AH) ���Wo{����V���Z��4:u�������E�<]�UC�8�v�&���<! ���R
AH)
�s��
AH) ���R
A��zNt��R
AH) �����y�zAH) ���R
AHs;�����R
AH) ��;����R
AH) ���R;��^�R
AH) ���R���
� ���R
AH)!���� ���R
AH) �9��t��R
AH) ��>w���_�P�R����"�Y�"��{ZcE�&�8��U.�~�v�� ���R
AH)!���� ���R
AH) ���g:AH) ���R
AHs�����
AH) ���R
B�{�����R
AH) ��o{���R
AH) ���R;���� ���R
AH)!r��� ���R
AH) �/y{�gH) ���R
AH)
�{�����R
AH)�@��L���<���}&N��+&o5�,i
��^9���n����V�G��|o� ���R
AH)!�o;���R
AH) ���R������R
AH) ������ ���R
AH)!o{��� ���R
AH) ���d�H) ���R
AH)��{�
AH) ���R
A�y��� ���R
AH) �7����
AH) ���R
C����zAH) ���R
AHs��?~|��W�������c���C��e���g������e��y������~��g� ���R
AH)!y���zAH) ���R
AH\�{��
AH) ���R
C������R
AH) ���R��z^�R
AH) ���R{���H) ���R
AH)�^���
AH) ���R
B���d�R
AH) ���R�{�zAH) ���R
AH;y��
AH) ���R
C�S��=������-q�B�b�c�YQ����}�v�1�tys[;����?o���>��#�R
AH) �.^��� ���R
AH) �/9{�gH) ���R
AH)
����
AH) ���R
C����� ���R
AH) �9�^���R
AH) ���Ro{���R
AH) ���R����
AH) ���R
C�y���
AH) ���R
C{���� ���R
AH) �<��������$$3����p��:���g���s�o�<�������) ���R
AH)�����R
AH) ���R���7���R
AH) ��/;��� ���R
AH)!�������R
AH) �����;����R
AH) ���y�w��H) ���R
AH)�^�:AH) ���R
AHw���3���R
AH) �����&t��R
AH) ���~��f_�i���f��U�� 9�]���������T��]v�m q�T|7=�7���R
AH) �����w�oH) ���R
AH)
���
� ���R
AH) �w��� ���R
AH) �s��:AH) ���R
AHr���:AH) ���R
AH^w�����R
AH) ����s��zAH) ���R
AHo7���
AH) ���R
C���a� ���R
AH) ����������;������!�������_�=���bC��+$�X��	
r�}�!��K���)���QG�$��������Ly�������$�$�!�!�%���
b�?!�d��q��C�����?����~{|�.}��!��!��$�	�C�\C���?y��>�_wd<d4E��|���S�a>���I
��$X0�B�<3~���������@���$��r�u����?]L�f~��D7��Hz�M$�����nas'�G����N�=Bx����8��3�q?'��Hw��OY� bq�a��+o�}������VBx�3p�����$��~K���=g��'��@:������$�r'��*T��2I�.@����k�����[�@��I�	�'�$���p�I\C��3����3����S��~a����P��B�{����=y�7���>a=�>H|���~!�$$���7��|��g������������� |�|��<����<����o��Y���~d/p���.HN�s��'��?'>��� ~d��!$B�"�QgW�%\�3�If�B�b�*�y��aYN��=������*%��

�N�����3�������E����I����Yf�;L��%��U�~�0�����w�3�����6\�i�q�	�K�e�1�]8Z�����
�o���r
����R��<���_��vk��(�.y�>��.�3h������csrw���Zgx]M^�T�v��������{���&�J��J��J� ����{��� ���R
AH)!����H) ���R
AH)�����R
AH) ���R{�����R
AH) �����{���R
AH) �����zN���R
AH) ����;�����R
AH) ��y��� ���R
AH)!���zAH) ���R
AH!!=;���~��y�[Fj
&,E���Xb�2������������VH�$�"L�
f$�3BbL��2L�1&I��2L��d��I�0W(�sf<}t`]���i\V��3H�z?=S"�J��V��q���,MF��hy�AH) ���R
AHm�z� ���R
AH)!{���H) ���R
AH)�w��H) ���R
AH)y����R
AH) ���R�{�w���R
AH) ������ ���R
AH)!�s����R
AH) �����z� ���R
AH)!���;�
AH) ���R
C{������w�������tV��������Xi�uSX6pM����������T%BT%BT%BT)!��{���R
AH) ����9���H) ���R
AH)�w��H) ���R
AH)
�{��H) ���R
AH)y�����R
AH) ����{��zAH) ���R
AHm�zw���R
AH) �����;�
AH) ���R
Co{�w���R
@�	P�	P�7�����������s�;{}���2;��.�u�5W�z�iT����2�K;0t����xD��<��R
AH)���w���R
AH) ��y���H) ���R
AH)���;�
AH) ���R
B����� ���R
AH)!��zN���R
AH) ����{��� ���R
AH) �9y��� ���R
AH) �;����H) ���R
AH)
�;�;�
AH) ���R
C��9��O����:������talT��{[������7���(�uM�����H� ���R
AH)w��� ���R
AH)!�s����R
AH) ����;��� ���R
AH)!y��w���R
AH) ��y��� ���R
AH)!���;�
AH) ���R
C����� ���R
AH)!�s�;�
AH) ���R
Co{�� ���R
AH) ��de�.����E_A����eq��O�GF����/R�%Y�Y�?��@��R
AH) �����w���R
AH) ��9���H) ���R
AH)s��;�
AH) ���R
B����� ���R
AH)!y���� ���R
AH)!�w��� ���R
AH)!��z� ���R
AH)!��{���R
AH) �����zw���R
AH) ���?�����O�?=��6v��s�7�jL��]��fr64����
�${~��R
AH) ������ ���R
AH)!�w��� ���R
AH) �/y�����R
AH) ��/;���R
AH) ���R�;��H) ���R
AH)
�{�� ���R
AH)!{��� ���R
AH)!��zw���R
AH) ��;��;�
AH) ���R
|
o��?:���bn���sr]AJ/���{��
�K�R��Jw����T��R
AH) ������H) ���R
AH)���w���R
AH) ���w�� ���R
AH) �9�����R
AH) ���R����H) ���R
AH)�w���R
AH) ���R�����R
AH) ���R�{��H) ���R
AH)
��C� ���R
AH)��#���?~3��A����+o7�J������4��;���v$��:[����p�����R
AH) ���R���� ���R
AH) �;y��zAH) ���R
AHw��I��R
AH) ���Ry�����R
AH) �����w���R
AH) �����w���R
AH) ��;���� ���R
AH)!��{���R
AH) ���������R
AH) ������O�>d=Isv���a+6���c8�y���RIXOX�$�rI�����>.�'�Y�����u�x���$sN\��?}"��JZ�,�������0'���a$=fg��_r��Y'`a.�{�{�I>O�%d#w�N��0���q�&�?!;�Y_���5���������x��']`q�5���O��xx	���8�S�f��7�u�����A���AC��:U���a'����@�=I��9.s�i�$nz�~B}� �gp��$�p��g|���sg�H^���� ~q!=s1Lo�v?aS�'�����^��&���nx�q��I�;w�x��$*@Y!��>d"�!)�<���=}��Bb?0���I�������gS����K�$�� V�Rs��	�q�B|�;��t�7CY�~@����s	>�$P�0<@��	/0��n�V�	1 V�?{��y�'�������O9��d�!$��_��]�p
�vM�	'����l��z��@?_��8���S�bI���0'C�� ��z�����z�Hq�HI�>���:>@�g�W)�I�+/h���U'g<�w	rI},����rt!��������F��� �2$��f����!N���Z��������;�S���j��
�S&�:>��s�#�P����J����%�A�B�*��H���B��5�Y��q!B�f��Y+=Yu��WXo���3��*�]uwP<���o����c�jv��VJ�#%��_w%y� �I�2����e�U�.�������nA��
���~G��R
AH) ��9���zAH) ���R
AHw;�����R
AH) ��y����
AH) ���R
C��y����R
AH) ���{���9�
AH) ���R
C����� ���R
AH) �/;{�gH) ���R
AH){{�� ���R
AH) �7���zAH) ���RW�}F�v?�g�q�����u��[���G��6c��^���:���E�;e��� ���R
AH)!�����
AH) ���R
Cs��e� ���R
AH)!���� ���R
AH)!��y�= ���R
AH) �9���3���R
AH) ������ ���R
AH)!����:AH) ���R
AHw;��K�
AH) ���R
C����zAH) ���	P�	Q��{�o�/����{S�Cv�Q�{F��i���ib�L� ��-w��w�����R
AH) ���{��H) ���R
AH)��z���R
AH) ��3��B���R
AH) ����9�t�H) ���R
AH)
�/{3���R
AH) ��{���H) ���R
AH)w���t��R
AH) ����9{���R
AH) ���R�;�����R
AH) ���y���������{����c�mu
���~�}GZz�-�SV��}=�o~��������*�*�*
AH)e�z���R
AH) ��{���� ���R
AH) ���a��R
AH) ���R�;��� ���R
AH)!y���� ���R
AH)!�������R
AH) ����o{�oH) ���R
AH)
�����R
AH) ���R�^��
AH) ���R
Cxo�v��������)X�N4�����tw�c�R#��:���[�n����5w]�?�#�I ���R
AHw��xAH) ���R
AHf��a� ���R
AH) �9����� ���R
AH)!��w���R
AH) ���R;������R
AH) ���s��� ���R
AH)!o{�����R
AH) ��ooz�
AH) ���R
C��w�zAH) ���R
B xc�W�|e��d�VwQ����A��/h�M��u�~����-�R���J���|) ���R
AH)���9�
AH) ���R
Cr��/H) ���R
AH)y���H) ���R
AH)�{�����R
AH) ������d��R
AH) ���R���� ���R
AH) �;��z�
AH) ���R
A��{':AH) ���R
AHv������R
AH) �����
��o'�)�m��W�fZ��vrwZ��/yU�Vi"����Z�~�~�� ���R
AH)!��{�/H) ���R
AH)����H) ���R
AH)
�������R
AH) ���s��:AH) ���R
AH<�;�s���R
AH) ��y���:AH) ���R
AH[��CzAH) ���R
AH=�{�s���R
AH) ����{t��R
AH) ����������_��?�o%u�5
��R�v�����i�2g��{�\��o� ���R
AH) �w�9�
AH) ���R
C����� ���R
AH) �.^w�zAH) ���R
AHo{�t���R
AH) ��n����
AH) ���R
A��zs���R
AH) ���9�����R
AH) ����s��� ���R
AH)!����zAH) ���R
AH~�?������e�<�y��V<�������&A��9�&o���kI!�4����|	G���$AH) ��o/=&t��R
AH) �����y�= ���R
AH) �;��{zAH) ���R
AH;���� ���R
AH) ����9�
AH) ���R
C���^�R
AH) ���R��z�
AH) ���R
C���L� ���R
AH)!�����R
AH) ���R������L�d�i��Y/!���7��\#t�E�Q':N�����lW�� xD
AH) �����;�zAH) ���R
AHo;�vH) ���R
AH)��z���R
AH) �������� ���R
AH) �7��;�R
AH) ���R;���H) ���R
AH)s��� ���R
AH)!��;�oH) ���R
AH)w����R
AH) ���R_���~�q�����<����>`J�x�<E��vg~s9����9��q!���@���$� q8���o��^!!��������Y��}�:�}��{����
d���?&�1��k$�s������~�����31d�|Bfd���Md���x�!n���3�3��H,�<`<HbC~�����!�x���!�!�|�E�/��8�}��{�C+�anB8`|1���3��|����}��
Ny��a5��������'����+�Y���!
`q�:����<��(z�~����}�	��]�'�k��a7��}�����@�	�1���������?@��C��<�g=�z���z�j@��S_��_��p���Bz�z�Ns?$'��I8���w01���?w$�����>�LHBO���3�=��c�'�����=d�y�N��O>� �6���}�4��gZ����#�����X�	�	$��V/~-���������Bnn�B~@n�����8P���{�����v$<@���0�C��I����������������;�T����|<n��s��.T�T�i�K������B�K��-�g/bn-8��WZ�5Ysd��K����=g4<�<:i+*�:��
}�A����+	����j����fL3�x���_gw[&�6I�z��O�a�������t]Q��a��y���y���wM��u�3����{��N��b]��0�b��y#�\a�
g�`��r�u9;�
��wA{K1~�(���M2���y�W]��!����R
AH) �����&t��R
AH) �����y����R
AH) �����;����R
AH) ��.��&���R
AH) �����;�zAH) ���R
AHw���oH) ���R
AH)��z� ���R
AH)!��y����R
AH) ���������R
AH)*�*�*s�������r��q����{�|U�����H�~�&���}����������������R
AH) ���{y��t��R
AH) ����{�vG���R
AH) �����G���R
AH) ��/{�����R
AH) ��;����
AH) ���R
B���c�
AH) ���R
A�{��s���R
AH) ��;��p��R
AH) ������H���R
AH) ������O��o��s������B�#����u�C�:q,kV��v#}����r�����^�����	P�	P�R
AH) �;��:AH) ���R
AH^�;�oH) ���R
AH)����t��R
AH) ���{�w��H) ���R
AH)��{%� ���R
AH)!���e� ���R
AH)!�������R
AH) ���y{��t��R
AH) ������C:AH) ���R
AHs�W�O��� ��������	oh�2�5W��~�j��e���{������$|	 ���R
AH6���:AH) ���R
AHw���3���R
AH) �����C� ���R
AH) �;w�����R
AH) ����y��� ���R
AH)!��;����R
AH) ����v����R
AH) ���R�s�����R
AH) ��;������R
AH) ���QC?���u3Q��Pn�I]�Sg5|m��������XL��NN��n���AH) ���R
A�{��� ���R
AH) �7/;�zAH) ���R
AH[���zAH) ���R
AH\�{�/H) ���R
AH)y{�3���R
AH) ��{��s���R
AH) ���oz�
AH) ���R
C{���� ���R
AH) �y����R
AH) �xD���d��'qI����l�U{�P��A������t�?"31����AH) ���R
AH\�;�zAH) ���R
AHs;��^�R
AH) ���R���s���R
AH) ��{���t��R
AH) ���w��y�
AH) ���R
B��{&���R
AH) ����������R
AH) ���yy��� ���R
AH) ���Nt��R
AH) H�>��`���_����S���mj������)�����5����2������9�v�y��������AH) ���R
AH\�;�^�R
AH) ���R�;��� ���R
AH)!������R
AH) ������CzAH) ���R
AH=��y�
AH) ���R
A����H) ���R
AH)
���gH) ���R
AH)
�������R
AH) �����:AH) ���R
AH{���>�j���~�k�-���T�g'e�������)bo:"��Y=x>����$|	 ���R
AHs����
AH) ���R
A����� ���R
AH) ���a��R
AH) ���R��zK�
AH) ���R
Cy���t��R
AH) ������B���R
AH) ����7����
AH) ���R
A��{t��R
AH) ����;{�3���R
AH) �_R=lz~����'���a�G1�8�9�Smd�
]� ��F'Y��'�M?P� ���R
AH) �;���/H) ���R
AH)wy��� ���R
AH) �6�;�7���R
AH) ���9�����R
AH) ����s��^�R
AH) ���R����
AH) ���R
C{���� ���R
AH)!�o;�7���R
AH) �������R
AH) J��J����~�g�����^'�r�N��:��:���X]}����{7Y���) ���R
AH)��zNt��R
AH) ����w��/H) ���R
AH)���� ���R
AH) �9�^�gH) ���R
AH)�w�s���R
AH) ��oo{� ���R
AH)!���L� ���R
AH) ����:AH) ���R
AHow��oH) ���	P�	P�?bv:�%����ci���~a'�&2��I<I1��1�=fy�o�g�?i
���p�T�����:��'�|�=q�a��3�	'REy���������o�j�����$'3���������}������hz���P�w�z�,���=~�9{�f3XN���&2I������Xx��;�]�?d�?s$�	�p&{�?!+0��|�IY�/�����J�k �'����s������<���$<Bw�_����������]�x�����<��	8�}�'�zy�{�~������I=$��!��=d��N�z�8���gZWU�R����J���J�U'q}h}��}�����������{�C�Y'=s�?<yr{�O�a����I�@:�H�fO92jh��|�O~������VB~��B�����]�HK���tHs���$����<dY�!>z���=u���C}�H@5 k$�}a��{���������\�H{�L@��|{t$�~��2�K��/�������$��� H��>�OZ�S��� Ad%@��� �$���s�\�t�o��g��+m��>����5�2w|3�!Xv�	��x��6�T���P�����_�������=�Z)J������������Ya>��Y�u6��;�y�m�tN0S��Q�X�k�1����:uN�����N$]�`�1�T��@V
cc���[�^�+n�������<�Q��,�m�Wc�������~cn��W"�(w��&�����rw�����j����>t)��|�����o|�3��o� ���R
AH)!�������R
AH) ����;�t�R
AH) ���Ry{�gH) ���R
AH)��zs���R
AH) ���������R
AH) ��o;���R
AH) ���R�{���
AH) ���R
C���L� ���R
AH)!����zAH) ���R
AHp��ym~�w���Su~BF��z���<|OZ�t^�[���C����o<��[���*
AH) ���R�/{3���R
AH) �����s���R
AH) �������
AH) ���R
C{���:AH) ���R
=o{�Cm�rC.^p�
�$<*���������v�f�;}D�"n�R��*���o�aP+��������K2[U'&M��;2u��2w{I�2uU�rd^�l��Q�d�R@�xMY#�xU����N������Sy�p���D-vQ�\�����30+�!R�FM�(�ymX��tHG���E{���2SI���FM��rd��#�vId�X���SIf&Y\J��!�M}�������-GK7w�LDq�;u\�>����&M���d�"d���'t���md�����&���d�d��
�D�G� �t;gb����Q��X(�u.�����;�����B�y�#2uH��Fbd��%�4Vrd�4IfN�L���RNL�*rxQ�g����]uG���u(��p%�iKZ����r��QN�m���.����}���92�#��%��S W&F���MS"dd��x�vo���-���:�(X@���������mfm��x�*AB������y�~�<����N�J��2u�fl�y�IfM]2vd��d���ZK���Y'�<
'�����Ok/��V�u-������������!{�,a����s��o��1�uN�>��>�M��������m��1��d,��>G>����}���[����m�*k|�{YG���<&V�\�����~��2��!���C6�"Cl
!��4�v�i��D�6��2���s%�4��~���3`LHt{����u�l�&2<�\�\J���@>d�w$�<��H{�|��qc&'_�8�p8�~��~j�����a q	�������<I���~a:�2�$��2C��!�!���<���zko��f@=d
q���@�p���J��?��i�?v��<z���@�!5$�
���{�!����[���jIp �';sgU���a����)�'��BW�<�q��w&~���XH��C��?n������|���7������}���%@�������s�����1d��������RE>`LI+���^��{����O�T$�0&�k!�$������7���������;����� W��n}� C�S�;t�����������_S�$ ��z���s����*�!���
�d���F�J�H}�V?��~� ��ORH�>�>O������b�!�Ad����`Hy���}t��;���O�0>@;��O{��}�����$��`O��$�I${�����jk���}�{�I����<`C�$7r� �yYv��f��}�j�������kt�O�+�V��}�c\aj����/g�x*v�/emIR`y�B&\�:��o.*��2!�y[���	KJ}~�������u��=������M�&WK5�W����7����8��������&v�f�q��{��K�O`��>��>bmZ���n����4"��+X*6;z��c^S�(k�zpt� ��q�����s�Z�o'u��RN�}B��9����v�8���?��A!�v����!�-�C-��I����[0���G�E������Y�����+��V,]����1��8��������1H����Km��~�����Y�����L��i)n�d3m��n[&��Ha
���?|�}��oO�7�u����w�V�sl�����}�8��!���wt�^o}����Rr��6��Hcm�!��Rr�c��!�v�1��HVI�c��,��5N�]'7h�Y��C�,I�@z���^*����P�}���r�ar����H[v�!��"Cv��!���5�I��JC����f��F���Z�K6��;(cX��=���%�Nlh�DU;/��@������i)�lHf��!���h	
�lH]�Cm�Cs���}�,��;+6k����w8�������L�.�hV���g!�����!�QH!�v�!���!���m�`�����jO�����A����L==��^��k�q1���ZZ\/�����B�6h�����Lt��h|r�����m�	
��Hf��HV��1r��)'�|�|�^/w)^�9�vcJ��1���_�����j���������,��1;�w��!��8�9�m�����m��
���fH��8�|r>��T�{O��#v�xm{�/�����C-f�]k��_��}����<r��Cm����D���C7-��2�������m%!���!�v�C���>/�=N����<eut7�/q<~'���b���k�;�����{���.���f�!�hRr�!��R�hav�����.A�@�/e��x��wW�V���!Y�7	�I����������nZ�~`U�J����N��@9�w�t:����������a�}�.���s\����`~�	�hN���`(k��';���� ~~@�p�B^saP������0�����=�c�����������*�����8�0��! x���9}��8� _8n u!��<��k���'��ju��rB$�A���$�*O?d���=B�y����$��CY	�����'#��=������*���?2j�s�/2�'N���A����d
���9�1���E���������x�c�����w�/���X0?=I8�x�E$ :�3����O��N����~�������0�o�B3������I��>������u����?0����8�I'��js3�>��7rI1����P	d��u��{���~s�����������'��d$�u�7�s>��)�z$�I+� ~����:�I>d��?c$�E�����k�����
�M@��$�$�P�VIRi��;��/�m��2V�n��Ny�`:�����A��������`.,	��c���ZpD~����{��Y������c��a����G&�J����p �����WSO>m�8~P�d���wk��ft�����w�xu��gWp���j���&�A�<������\��gT�.k9���>��1a��>'`�N�
��*�MOk�:�V=�����au��jh����oVH1j�b���9,�yy������.��]�D���a
���
�Id2�JB��B��6B9>���,/a����6t�������m��M�����9w� ��9���/Y�1��H[v�3n�$-�d�
�in�$7r��3n���Cx���]^�87]`��w��9����2����'k�e���muo�xU�����i0���$7r�a
m���I�m%��hR�h2ow~{��~=����o������
� �Ao�{�F�o����x�}�������l�Cw-��2��HV�&���a��!n[�m����o���y�}H��R�v
����e����2E��BC���X,���z�2��1�R8����cm�!��d3m
C-��m�)n���'���y��o7�_��=�{i�K��#O��#<���A�J��L�v[�7=���|R>
G��6��[�Hm�A!�����C-�O�q��U=:yJ���Y��3.J��^������n�X~�VT�w8����Z��y���I�+m
!��4���Cn[0��l��l2B�aH]�I�.�_�xWv"�����2^rC�J�7zn*��J���	��LM��>��xf��d7m)
��	
�������-0��ZL�����l}����^�$���:K��^�oF|V����]���{�O&w����K!m�d7m����I�+m
!�v��6�������A�MHC�����J��y3��}\���w]���{@?E�Y�0�5�+�;�d3m%��l�+m
��l,��B���bB��i
v���B����n{��/r�!\@?08���q�VH�&"��n?{��1���R�@S�~~g�V�@:�T��<�6��nwx���_;�����C�
��!?\����<w;��{����$�	�B`��������{�%����:�/�d����y������|����OX�n@'�����7s3����C��'}��fB~a
�x����j|������������	��XY�}�������z��z��5!S����:��~���w�8���� �vB�{`xx��=�>���@��>�������<�_���@�	���$��C�������yq��R�bH)�q��������/<��Vt���������+�k<C������!����Hn�yd���R�}vf����"�*C��������r_�_�n��d��>a���Hy�����n�d����&��2XN\ @:~;�����;rO��{��
�uU�$�����������d��`�C��:�(���E
�g�x�9�r�=E��I5��I!��}���g��7�j��wi�/\Z��������
C=wwr���{����#�I�\���^<��o�A���7m���<�Io����Fw]r��[�����uDL{����8�u����dC���&���xh��,��Oi�����-j��^�b��X��[�:�eJG�Zp��������-a����Z�K����B�]	��q������8�������W������pe��S�Le���Cm�Hk��������0���Cv����e���)��!����syc���EV�g>c%�w&h�x�K��=����A�x_3�.wv��!�i)
�i�n�v���0���B�[[v�C7-%-���f��<Q����\��EM�~Q�w)r���{�@Ck�/��	m�������	�ld2�
C6�����)
�l
��B��!���<s
��&���\�#'��.���JvK�+0��4�2�U���c�nn���6����a
m��5��
m�i�l�B�hi
��H{��g��?������yNv��8iv:�BjA}j�j�����������������m&���p���	�n���!�i,�[e!�-�����{�x���=��qk��.Lk����V�P��?m����������V9�����9�#��[0��ZCc���m���[0���i
m�6B�IHo����^}����z���T�n���������B�KF�Z��h)G�^W�C����\�>n�$3n�$1�Cd2�
B���n����
!�-�C�^y��}��y��8��?��N�:�BX�����CsV�gr�M9����jO�C����sh2m%!�v��n�!m��m]�c����C���Oe���X�_h'sT��Jt����!�9<.�v]�+rs-U��zA��l�+m�Cv��nZB�n��v����B��r�.���9r��m��&��O2�|����Ug���&E�%����J�;����>r�����[m���JCn[0�;i4�6��n�sv�C��r�������C��ko� �>�	5��&�y���_=�����7�$� 
u���I.�[�3�x��$�p��CC����a �+!����	������$��!�	�{�@>d;~����������C�=BE����	w>�O>�{�u?}�s{����'3��b|�'X����\�+3�=��s$=d�9�I
�{����u;�}u:����B~By���<����sS�!��}-�u�B?d	�� N}� w�S����C�!#|��'��N�y������d&��1L!�$���&�	nW�'������	����J����Y	���<@�}���Y������
�����?>�1�=@�sRO��� �O������{��|�0����j�����2<���\�f5W�����<@0'|�l�� q���HMCd���O����2O�$$�?{�?>3��j����C��q�:�x�!'�?`z�y�'yw��2HT!��>L�����s���,gU�����J|C�'�������~HHP^��g��`�}�,���g��|�^+q^��r��[��������ackQ��5��/����1gj'���HwG���t��9����w>������a����[o����,�@�����w��s�L��2���\��j����r���
_.���U��p�w��OF����M�R���	�/{�Q��m�����J�����L����_�2���]�6~�������r��^�>���[��m�U
��JKl�<��0{��{�����l�m�HWm
!m�2�!�v��[�D��l0���|R��.w�~�9Y"<�OQj��_0����n&��LfS����mms=��9���}����n�,��aH[v�
�a�+m���-&Hn�JCp���
����&r��l��>�9���/l�K~kn����;b�����������M!�-�[v��m�)��CnZB�ZC6��hg�.�����gx�v��+7���^�9S��}H�-�l��$J���gws�3������B��d����m���m�n�!v�
����v������m��yJ��x�&����$%�A^����7o�����!�zwOA�������R�l
��d6�
C-�"B��	
����[aHV�
!���B����Rz�V�0�Y���r|8���7�io����R�[���R�?��
!���c���2���3r�a��am�d3m%��iHc����1��{�=-=�"�\�iO:���+���Guv�l�T����v����_Cv�Hn��He�I�-�D�m�2��C7-!�-0���L���������(��W��6���aE�F�A����}w�Uc�$��R��@���4���$7n�He�L!����.��$2��2m%!�-�CO=(g)��um����*�WT���������g=���wk��W^�v�i
���
���[���aHf��r��7r���a�=�36�V�s����,�}�nN��+��4+r�r�J�*��UL�5�T�y��B��v�!�v�C�4���HWm
!��He�f��A!�-�0����|����d�j�� _�����x�s�����������`i���K����4�D�D���8�����u w��!�M{�	1;�?������z������l�������I�!�a��3������Ld�@�qI<E���0��������� I��R{�}��>� @�:�7��x�'�������Y�
u�w�o��?0;�w�s��a=d��I�	������?n||�������������w�q�Op����BHN�:����#&'S��3���wI���q!�^] ~�	:��S����7�������B
H~d�����0�}���R���P����$���$����x���'�=�O9����r�7�����<�q�<����9��sP���:�;�:�OX�q$5 q~���i^��;���H�%��C�&a�������`���/�!����@=`I ����rx��������3��$����c�~sw�r|������@�2�N0$������=Lyp���������~�I�:�zbc���=��R8�[GVS&�9�&��#.^�Q��m��XZ���l��U�Zy"�	>
����u�+]`/!���d^t�f^'c�=P/3�xUr6��q��k6^9�
9�xH�Kp�a'����k*�GuWmB����}jLVoTo;u�6�P�'k�gM����x�S��U�1��x������Wt+��K�%[��xI�,i������h���kY�=W����v�i�{���&Wu-�g��i���xVt�]|V��1��73i0�;l��M!v��f���M!�v�!�F��������1�s�p�l%9K��x�Z3j�ej����o	���d=g��
��X���i
�l�m�D�nZL!m�"C7-0�[�2�)o;���s��+��:��MG����^+����ww��\��������?am���h$2��He�A!�-&�-�C�|R�'���R�y�m�k�*�:>�;'�a"������,��^�����e�<{����i�ir�a
v�4��ZL���R�i�+m�C]�|��G'�A����x)<	����\uk�ks����&^�f�����e����|�Cw-!��4���2C-��2��C]�i
��n����#��
{��f��I��]\���|���)�������s��N�*�n�C�g��m�Ce�I��,�[IHcm&���iv�i
�i�Hs��[~�~���������N�w��X��qW����xU��zu4{��#���4��ZHm�)r��r�0��i6C6�d2�
C-�����H��SU"�2yq���Sx��VZ���t�gF�L����:�|��>)a�2���+m����@�����������d�����o�~u�<�����Y��go^8�����y�,~����v(�*���1�QI�C��p|�����m6C6�d3m�!������w7~��������R �4�U��8���yx�]��;5�h���2rR�(���P��������H����Hn��a�lHf��[v���|��)'���}���>��{r����(uJ*{��vb���F�4�Jju�����3U�������[����u���S�0s�K���P(
s��_���sqH��������E���>������H@��=���P��|[��oR�P9�wy��~���Q�F�~�}�T�I���K����6�<�_�Q0yo9q���������f������DH��g�������G'��Q;l��]v�k'9l�E����A-��$TZ����B!��\�ey�lHsY�xb�TD���{{���s�0V	{���w=�*�+�_u71���rg�_���i""A�n&/���������a}��<�y��"E)����z�r���y�������<��/���w�m��|9����S�n�m�{	�N�&�wA�FV*4k0L���B����v_�2�z���r�����{��cCR[���>�}�_�"BB7����4�"�DTF{�w�/	�ADE��<���*�,�������C���#A�k[������ ����8�G��g�w3����)"��F�r����OkRI
�K��YR�K'_(�W�,���,/7���H�� �����-����>��$��u����w��F��"}��{���d����p����o�>��'
!$�����Z����"s\�Y���7���~����,����}��T��9�j!������|�'�g��zwX�8��!)RGy<Az�P3���I��sk�Rw0NoN\����xI�[.��{��l��cu���P���s���|Vv���:#��y��]v:�5�vQ�5#�6��~<�/�W��{�V�j��(�Z���(����-��^��TF �D���36�{��0A ��AV���r]()����o_w:����E�w����q 	���-y\���Dby��osn�5DD�DDK��z��A�}���jl���7#�k|!:�s������mA{�^3qd���'�+���FQKU��fZ�S��XQ/V@��n:^��^3�)����_o#�3N�vb�Z�^:���w�Jno?c�`H!��|�^1� R�D������*}�O����"(N���1��8�K\�si��}���L{����x����s�aC�b�
f`��s�����~������r��	��������q����5y����A��V/e%S�qT�U�.=���5�~�*�\����^����xo@.�(]k��[����:��6�DD�!��Q���X����B(�=�_�>���N���)O������}5��
A1������>��D�
�>���
"(�o��Nn�uc�s<�[Nz�lA^5��9�������]������s��_J��H�7Ebz�JG+��`���C�6����b�k�9.����7=��s=o�t i��*�o.���E�O1���0O�@$�L����������	���c+�����T%*��}�{��M���7)
TE&~��n��>f�v�@�oz����X�F�!�L���y�mruQ�_��3mP�� �M�S:~f���n�����,���n��p������>5�}Z�����M/Be,/3w]5�e�+��oz*@����*-�)y1n�{g�a�A\{���c��S���I�H$��|��X"s��t�����3�����""+��?r������DED�}�fHP�M�7�x{�����0@DQ���v�n���I��5}i��|H��D���e�������������������D�T�|g�AV����b	�;�w�x^M������n���h�]�ay�^`��������}�{��x\�����{�������)I�}����VDRH}�wz�y���0R���������w6�=��D�"L������"���=�{]�("H�y�}mN�"�I$��vk-���,z�L7�����������{DC����f�R�Y-v���h����wmT�A�m���5~{�=��|W�\�������7{����p]�K�S�,�{�t���U�>�^n�J
�W��s8��=�D�����W����R E"o:����BR�cq�s����@�3>���� ������MQ�w��x���DA$��O��1��"�/zP�.��{��L��]��Cvx:��l����s�k�������&�����Ihd+��%��R�e���o���l��+,[h\4`�<Y�=������^d
R�����g<����WE*�4�{��w��! D�"%�=�����E@P������>�����j �
v��b�""R��_3����\��!�/v������A��'n�����|�����@��p�-�������P[uG�M������$(���a���g��=��%����S��h�1�����7���8)����Pc��?~?s��]�0���[����o2�X(�b���wy�n��>�>��B��"7�9�k�sU�B��_o�����o����$H�'u����6����w�M���K����Y�e������2�$�5�sKz���"ETH�����o=M1�/]cy���k��o�")�>���:k��x������u	DA�F7���F����X�bc��9���������P���,�]���<���^��]!�t���J�TEB)7�;w-�{�����^�����=�wn���\R�TB�B*��>�p��*`m���Q����R����\gy�9��=����X��]�/��s�sK�)!!AE ��Cn��<|�M�21N����,�U�	> ��|WvsU�����2w��J������w�J�
����(�q
�m�/Z|�jzyF���&��)�1���0��F���5��XZ�3;V�{�U>��e����/��>$�u=���g��$(�����������"��w�8��D"�9�f�����%���{Q��V�������u%�Q �q��L���B
A��8�K�e�IHH�B)@��._��:^�����q��wz��j
*
���y���Y�4��r���_|������K��]�&�i�	�:;#����<��A �H���=��\�(3��j�#�Q��;-Nmo����R"
 �)��y����|_��z�i9�o{9����zb�)HBQ@��i5;y������kq/�On��
�WS��t�'�	��H�r��md�/Z`��!�APp�H��R)E!"����u:7�]Q.�����;��V*��}�Q�A??��}�)�&f����aw�U$$@%*";{�q�s���;w�gN�|i�����@a^��P�+p�s��:#b[��y���i�b2�x����vG�P�'�������M]��jq�ut���i�*�sW`�"�(D��v����*")Y��Z���A��c��9J� ��u��P�� $�I�3�����_��������� B�;�}�Lk��E"������}z��b
A)@�D�k�����nk���/:���� �J���g�j���$F�3`���D)X���������g���e���nrg�op�!B$BAC�b.Y)��z���o%�u"��)��sX����F�n{]�5��7�=����zH���P�(P"�@N����r�O��t/�c����������bc��y��wu8�]��R��6���x�H��I$��Z�<e�����^�,�rBa]�����DB����g/��v��1uj�%�<����	E(�P�������c�ngX�{e>���E#��y�U	�a0��z3��Fo�`�}S�C���a�x�u��fv�v��~� 8����!	������k>�+�pQ���S�$�G��x���[��$��$����@�j�v	R�����nr�"�������������Bn���oU	"����c��;JP����\�B�k=��_��k�m�����*��

z����p�����J�`�JFUA�9���/�9�����%��B5Mf�wdJ��s��H ��Yt��N���/������gR�����o|��!�"@C��-p)�{CR7��a�b�h��(�!H�/���<�/��sx����.���!�U(G��O��H ���7��
���L�;�:x�@@�!�����~�cY�������iz�����m(�UTUt�`9��u�*�������;qi��A�DDD$���k��r�w��/��$H�B����b[�n�V��x����q�	��5�R�#���N�.�����$�]f%���z��c��R��j��i��������C��5<�HD�ox��g���� �#��g>��}�E �x����}��"5�e�a��Rv_�3��P�T��MkY�]�5$�������f) 
H����+����@����Dk����Lh�A�ay�L{��$9�@�I��E��PXT�HIW��������;a�s��
(DT�9��r���a�B�����i�Oy`�n�Hi*)B!��]����C����ywKQ%��v�� �$���(,rq�M���+�=��Gx�8��EJ����9��^�02�a��!��Ws��}��|�
�"���5��s+��o��s���Z�u�(�D5���B"��l�y��-	�&�W
P��D�"!~�q��n{7�kW��j��������b�D���
�#���1,9��
�3Z���v���.�Xz�n�=\&���^
{��w"B�&��<��^�	�T�6s2����;��J�E,1�l��P�-���4�v��[��"���G�u!")���f�����r( @�������Uv�B#O�2�o��w��=
R���q�s)�1�z�k;��{�bv)Jo��M]$f����}�����ATJ������\�3x�1y�\�s1����g��^��J
Hg��w�_S������5}��OS}������QTb�1� �Ej��k���58��Y��������1�g7���"�
"����*D�J��|�pX:p��|��w{�R���B	�
�e��R��+���[N��gID4�d���AR��$��������n��u7��|���HDJHB(�Q�NW����9��f����7{�y�;4�)H���#W��-l\�<p��7�����g�9���E" �H�K��<nZ��lwkv�o}��Wp���A$�G�{'6��*]H ��c"3u��t�t�/��J�����'������������|������-��S����-o�'�=�u^�&��@�����v��u��Y�k��B�L�����q���PDJu�_��gT��������b��BAJA����%����	T%�e� @"��������Q6�����kp�! ���3���^��a�w�WV�Q
D ~?A�%�������l^8�������s�)�����/W��G�;���7����,�C�^�R�HU@D);����7��{j�&�*�7�h��jP@"�D ON�S7��,��w��=�;��Y�7�z���B!"�H$#��E��|;^�y��;
��|�/X�=��y����M�T$*$$+:�%u�~�h\s�r��fg��b R(�D��^y���>�=zF�f'�d\�����H��$�D*�;w�������c���u�>���8s�M���#��Q#�@������<�Cs1@�|�+���*JT"P�4��q��m77��\�Od����e�a��MzX���9��H.�P
��L�&C���A�P�R�$e��!��3��f�\�-��?`Y8T���q�A� �{�ww��c�$DH!z��:�J�G}��_���W �	IZ�3���c�E!_o;�'y��@����������|~$�� ��X���
s>�fu�z��O��R@�!Dw-v�zc����o��jw:�<k��f���0�<���A �@?�R)c��[d��f$�AJ H��)����1�r���s��b9�gZh�P�(�D� �C�v������s�c����sx�;�o����"�H��!H@��v��7�Nk�\�i�"���������s��oW����������x��O"")R!_PP�=(R��^w5VK��e������v��i�s]�D(�TR���WT)]�]���I���x���;��&��()"(��"K�����{��s��39�r1���9�B!�O�:�s|{L����SA��w,��3�f��i�+W����	c�w�����n������~��E�������_ M[;��^|obO����g:���>�5J�:��&r����U!M^.��r��o���I�/���3�W�����)E!!\�������W��$H�(����{~���bD�
EEE=���9|�9�� �������.���""B�����7�w_�* �T�!"�PDT�����,��G���^?/��zo6�<]��[���:�L�l�k������
7�j|��[���G9B���e���9l���Z��S�o4�\��F7�I�e�/��q9��{2��;�[�����2��v:�c1J;�
8�Oe��b������,�IK�xh�~��a�=�V����}G�["�X;6}p��eG�lx�Rq�������V��\���2MC�JKu
Q��N���;��M��)X�+������7Qy\A�=�[��uN5�U�����HuyUQ��ol'!C�v5��;��[_E���m-�.�N��,���
)O-wS���]���iQU���
�8h F��X������yZ#�F
�=]{]w�e���|���=4V�����wTrL�Y���K����x��=��&��`'��"x�f��2��X\E�=�������R�&��/B����-�T!.��������PL�
���2�d��v�z�t�!Ks��>��hrY(o�������q��K���_�P/�]�#����hu&�L�Y��V*�
�=�r��Lv;�B��w�\y��b�bVA��v�6d�dC������lQv
TG��U��JV������N��[sv���S��5��-;�
����������]r��m������&�����n
�8�Znb�7����Z����H�-W�k������yS0t�3����WZ��8o��\�.��x�8�E�pVa:zu�����:v^Q��^���w�Ni���x.�;�v� �o:Wc
"��D�#��E���T�]�dy�)\X:W,B�}:gll�9���x����u�6j�.Gy�[�/����U���RY��/Q���,��-�W���yU�e���u=1V���N1��z�5���Bn�=]	��m���y�K{���"G9vo<�����u�|f�13#;���u�S!����-�������W�r�Wi��5I����6��m�-z�,�4���c��_&���_v��Iu����"Y���qk�]�4�R�[���M+k/oVx�v��W��� �7]�	�<���jm���m���T}I�J�T���M;h^e�s��CCv�l)v�"r��8�E'2��$�h��R{����~�����s����q<�B����z'��[3���BH�|�'���R�������16�Vd&1���br�8����Z}w8g}�M��h��f�B�Vqz�^��)���AT�/��WO�:�8��b�����u� ]v��/��oN���We�g�*U�����4ov�	;��0v!�gY��Rv7gJucjZ��A�M��������V�����8"����U��J��ar�h����P��kb��|Y�8�a�B4Th��)B��-�|w���s�/Q��f#�WM
6]�����)vl�a6C�%�[n����$����+��\U���#���$��T����!��%wS.�1�r��NZ�vK�������
B9�7G�_^�;���A��h$8V�.=;F����r����mt*
��g*���F4���$�.iA�v���DM#������}B%�Z��GC&��1h2��Te)i����-�k%q�1��������I@S����I`v���1X��������;�R�p�����X�m�@�[z��;�8������$�yt�]����ov��\�w�����4/��3G<��e!W�5�;_��!�]���W�FeH�L�*b�c�MQ �v��v��`y�V�5���Yx��l�W|�x;P;��A��Z�#g��W�9�E���PY�����3x�����Y�����](�[���WX�I���Sr����7H���v[��Gs2�U%����S�r`�8��s$`�!ZV��&9����H��>�\�1������S.��j��cj��3�w^,b���f+�,�7���m��&�Q]ZF-G{�������3��]E���	4�;�]da������S�+94��,�v�����.�&��5$���[Q�J��2�M���F+���>��;������6c����!���!�H������/�w�t���~�	�9�!J������g�z�����q�%�����U�����w$��O�J�=}����u�����g'Y��d0�H?n�5?<�z�w<}�'��Y��<I� ~a'�s��/�_���>rtz��	5!�'���+� ���?n?a��z��`d�x���O��O�!��O_��'��o�:9�t�u�����#!�8��v��9��q orOnH�0;�C���\���������I� �'9�~��s�6��5�����spx�=�C��4'R}�V�=��s�1$�|��2B3�H���u��$����RI��L�>�.�nI�d�=�8��� q�(��!�IY�@�sp��[�)�?>}�����@� w���m��'n��������|�>B@�9�����_����}��	��`����Y�\9�C>�
���B��$��P�Os?~�)�����HO��%@���
g�k�?$��8�� L���rBz�`E$�E�&���Y�4��p�L���8bT5�X�v��f��u	���Jn�;<0h����$;�+v��C�:r����_`���r���k6�'��]��� U�
��\z���"�����w��jv�M��I&��W�.y�C�I��D+��c��+�0���LW���:wi��]�/VM���#��y�>
��.L'F���S�hG$v�e���~x�-���w���p`�7�B�dl<���Y�t=wx�,j]��A����H��|R�!]�6C6�Rr�a�h	
�I�2�����C����������{-n��iJZE�%f���j`�V�D�����z|9L[*���.�>�|7m��nZa��!�l�.���]�JB�i4��[�����w��������� a]�M�!���j���Cj|�`t7o�u���.@�$|V��m�im�H]�@HV�&��jO�	�!��^�������1>����;}��

c�0���������y������!�m��i
B��$1���.�d3v�d-�Rn���p�����{���'L�9.� ��XJ��t�}R�4��!��u|��������C-�wl4���D��ZCw-���)�������Uu`�|[��{K�ngr�v���DS������Js9�t��y��|=��:JC7-��l)�i�a�1��Hf����I�m|��n�B�>�or�K�����	"�;�]�D������F��!���S:~���i��	
���m)
��7l���.���3r�a�qw~�x-.��3d��;�.���*2������d���1�����M^P���p$3r�d��e!��Cw-�$.�)
�h$3n�!��4���$;������n�NfSL�(�#�J<r�IIx�W�3������[�+��jO���2Cv��3n�Cv�Hn��Hn������������t8z���;�b��A������{����.����n���_ou��}�FB��!m�D�m�HWm4���a
v�l�m��m���q�����w�)[U)�~�}�H�wzj��`h�%�u��*h��{Z�-����j��.%�z� P��Z������(z�1�b9�=�;��O]�)�/6���Tk����]�@��x&��������9���}��EbsSF(t�x�?w��1�'�u�����zq9����r'��sfP�k�p�@2d�6u���W�c����X����\���������$��R8�bn���(s7z���}���7��l�8���=�����j�nJ���~�%i�Nd�'U�-������5���(f���2���L�Nm	z����y�#��.���H���b3�$����������y�Sn�z�M��mH�k`]�.�!�6f�.���}��v���t������>,�����@]4�I��q m�}DVa�_�X��{�e�(�8���r�7r������h��	�<���{����A�Q����Y��z����<�����n��-Y�{��w��!�����i��!v��!��C-�$+��HknH"���
�Uh�W����+������Op�\�����)�n�Vo ���w���2��!��!�l���CnZL!��"C.[0�[�C-��VlHN�]����T�"��
7��f��c�r	KV�����U����
�����f������bCn[0����n������w=��~�4�+����D�9��G��X*�+or�:�Kr�����|�O���5i0��h��v��;hi���#��>�>����T���/�X�}�L�r������hc���:
>t._-��y�5X�s}��|R��(�D.���
�IHe�L!���He�>	�>|$R!����U�u���C��=vq�3�2rWL��{����S']����w���!���!w-!�v��l��HV�6C-�Y������z���W	LZ�w���6'k7���������e���cf]w����9Nw?{�>4�m�$3n�H]�L!��C-��m�m�!�-�����J��'�����������<�&������j��Lu�"tf:4��7��cm
!n[���)v�i��a
v�i��Hn��$-�B�����z��3�T�
L�t����+8����Y&&�r�^�u�=#��n����)
v�6B����v�!���r�a�l��a�7�����x�{�	+����2���#�qF����7�u��6����`?�Hd�����h$3n�$2�����a�1��!���	����?y������VL�;L�v8�F��t�������
�W
/���
B��������{�^�|�,�+���j�Y����X��Lw]����Z���}��������M�����R5e�Ly7C�a��8�?{7�y3��A����~�<B��.��������:��8�b��{��g�^]�0�(�Mb����1����,�~����v�_k�����^�
1<���v������4 <=9��H�F��=��}'�
�H,@{��yDu>�������$�S��\��G��Q���.J��q�����������7��{,�+6=Y�76����Z���y��:;�(��b��I��[;v��;���M*���#�G3e��/.�az7l��m��,����r�Et6��F�-l���w5��J���\	f�,����@��Dl]���.��Gz�������y��}k�|���[�1#smkV�^W1�q8j��.��0`ZJ�>g�{�:w����i�lHm�fHm��1�a���C.[2B�JC�C7sj�}^�������i[}�g6�
�k�fDuG\E]�d�y�2W�.5"B��L!��B��$6����lr���@�+m�C-����.�T7��/�L���##m^'4~Bta���e��CV�����u�n��CHc�&Hf���i��!������3s`����{�2��o��11�>:^�;��;l{�������O?<�_�p�~��c��Hcm����m)�n���CnZL!�7`H����
��;.w4F�����K��;��>��W��hz���������s��Cw-�f��Cv�"Cm�B��FCm�Rn�d7m���O�z
�]�������U�,����8�y��^�Us��F��=���j��I�7n�$-�I�-�I�+��d-�C6�Y�����I�+'�i�����:�k��3�x�����c>��[�U+H0D���t��>���m!v����fH[hR�CHn��!��!m��y������U���M!,�����]����Z�2�z�{�`��F��h�q��d���i�CHcm
!�v��.@�3$|��U��w�n����qx'{��W:���,���mt|�u�������B�Zar�d�[i��	�i������$3m�������a~��1Osww��wk�4�i��{�S���9�^�,�
��m]y�^������a�-�C]�A��iHf��!��Hn��$7m��n��������r�������6�%�X����������)Am����������L���y����`=�'�0w�4/��b��*�p��qN���B����+�����Y[_\AF��K���3�������+�8���!�7������>��*a���v����>��59�3�T���=���;.�'�e�#���Rj7eu��x��Z��	�C��,o�����G�����#�4�-����P��=����j��!�d����T�{�wM���f�����}*;�>�;v��|���k�&���IX�5��uf^m��{C���pn�&B#�j���=�y�D�Cv�pA����(Sa�Y�!�qv�eR�O{4�9E�f�rX0�0�j���W|]):�d�~��]{$�x��z��<*�t�����rd6n�pL���
C���]o��
���]\�n�T��S[��0>b_@j
��D��iJk�����#����LU]�;�>��~����He����!��4�6�i��2B�IHm�@������(y�]�f��1�w1��������M��]t��q�"b��
<��}��|o��!�-!w-��m
!�t#�B��v�����!�l,�f�V�}��u�zi���I]���wG��uvt$hl�T3�3r��e�r�}��?sHn��Hn��Cm���l0��[&����.��a��v(x8=�T	���2�%o	��������u�����E�KC�j�8U��<�?~���-�D���2C-��-�A!������Cm��6�i
���5q������P�8oj[���Wq����;�������7m~g���~�a
����Hf�
B��D�\�[���2���v����X�U.��lF:�?r�z;�v���������:O<����*�;H;�7�z��jC[l�!��R���1���2��!��4����r�a�e��w�x1�w~+�JL������e��1����]�+�c���9�I�1�f���i�l�r�2C-��5�;�`����������;���n��*�oHv�%4/{�)���>�g~��g]��"C6�C6�)
�a�5���.��C��C-�$+d?�I��|<�o�u(:n���Zfeq���j'o���

y6ve��?o?vK!m�d2�)
��CnZL!��
!�v�!v�Cm��n��V���[�.xm����vl��w[K���M�}H�!��	���JB��0�v�4�6����!�����������m�r��_��T���M��XZ���{1�~�f��M�w\�����v���b�Y��c�:W�E�����G7j�i�(A��5�?\����%O:���[��o��:�������A�_�7 �r���C���u��iqL>=�3<>��~k��Y�y\���^�&�p��~��Q�r�(lx����L��G
c���8����s��=�������-�;��n�h���q��yN>Mi�;]�{��Lgl�2*������xx+��OPv�4�����Hs{�E�1L&F.`m���a-��&�]U����\�c9�S'V�)YzlBf	�\�������E����Rj{}d������M;�
�V�P�GAKb��rr���1w0��5�@d��wV3|1,G>�f�	,]��X�\���v`��|Mu��L/S�H�7uy}[R3{d1���N�u'����������in��4������@<Z-�����E���v��U����sr��krR�U��<������5�_JA�YDg�g�>
���-���
Cv��C.[	�-�C$7m%!���!�-2C��������pb]�v�9C��vC���=��J#q�����mw,������8�|#V�
v��6���m&��m
��'�|R�� ���Y��t�^�n��gi�������o����-��������3�CHf����jC[hl�[IHm�f���v��1�f��w~���<���z)%��t�u,�e.C%�6�q!��xah8N}j��o��!���7r��-�L��l�1�@������R�M��hR����l���ko�J������)��JV��4T��>W7�>wg�s�|����ha��Cw-!�����iv�i
��$2��$3d�R�o�
�Y�/��cd�b������D�]��xPQ�eD�w	�����>w<��w��-�D����!v����Ccm
!v��
�����C�5 |'�k���&J�sw�`����������V�zIZ&+S�������}�����&��`��-��hR�l��B����l4�m�A"�|��/�T~���9���e�Sv�5���j���^�Q�^]��n=���C]�A!m��\���l0��l0��h2�)
�lHWl���qV����B�U��H��p�L
���
��G����]K"���[cm�Hf�����a�2��!n[0�n[!��#!�iH{�_�e�B��Y,\���f���Y7M�9H�������(m�PygX���2O���v�B���6��$7m%!�v�d7m,�m�d6�%!O;�����s������b�@U������x��S)���S�o�>������;i
O�K����}Qf����v��y*�*^��<t���9�c�J��� M��9~9V�E�S���h���^:
�w1��!y���c'�.T�i Om�W,��&�q���U��h��0�C��xsV��a|m-�`��Y����7t�+�=�o�ZH�iH oA�U���E����<2�����iB�_��eW���x{�U;�.��/T�8;�Q�������^�+v2�.a.EH�<V���z&m����s�Y+���. �C����t��-1�R����U�sd	��e�k������+�3��6���9$^h%ws"���W�����gc�zA�N��J}D����7=:�-�����9]i��[eR8T����j���a+i�3WJu�����.��r�E�"T��w��=QQU�8`7`,�$.��K"���y,F��d��.��������d��B��m����Cv�Y�����
!��Cm��>�����|,{<�b��2*���{q�gxM���d���u$[�f�W����>��Gm&��v�B��Cw-!w-��q���I	������{	uR�^������w}is��e�3'�v���x��|o�H%�Z�#��p>�D�n[�.�����!�l�.��[v�!����������9O��=�,eI��5Z,��ex�Ywb2%Z�5�v����/��R��Hn��av�4�nZ��
���R���$����9>|!9]�{�����������M�I��.
�K&�
O���b�V�c���d|!r>
�|R�5�`i
�CHe�d�I�.�\{���"--�a�NT�x�So1�pn�$[�r�5����]P��e��2�5S���y�1�fH���yhd���!�-&�v������ �'��n��N���t�f�>��gR�:uo�WL?zvA��*l�Q�9������A����5��Hcm�Hkm�Cp��m��l�+��H[7`H��r{AZ�&���Wn*JU�����9�7��3^�FX7z�������'=���n�������>)�|!��d-����$-�f��i
d��>��z��]m�J� ��.�5+���K�����EDm��r���U�5�~���B��?�F>
��He�R����hCw-0��[������d�uJ[[;���w/������S��9b����x��/�q������l�C7-��.��H[hRn���v�Cm�Ym���7~�xK�9B�g8S�*�f������E��r��#�3���f.�|��3���2,fw�����r"�'|
�wJt4j��k���X�|�D��Qf�@
5�nIt��\K�x��n^����i�l���
i���x���$Aq"zM��R�M��S��h�N�Nd:��`J`{���v���%�z���"�(�q�/=f�]�
�����{�E3�3r���e����f�2�F�{�{&z!�����&�|<]j��B"6�]���9�2Vw8�I�a\��&m�����������D�s��b���B���ki��g3.�����1^�on�T�j��G���7��q����
Z�J���WsM����ot��0E�4[K�+�^�>�Ge�iQ��E��a�=Ts|����>��{�>�����im��u��$�N��+�%p�6����t`�����.�����l�Or�,���W��\z����4�����9����K��k���%�b���������>�����I�5�$7v�He�f��a�3n�H]�bC7-��9�\C������������5� ����n
���y�������$5����l��
B�him���K!v��������B�{�C55]g�zd[�7a���P3)?�k9J�����m�..G�����!�����hRn��\�a
�h�m����
!��Hy����s�����F�W�S�����E��t9��\F��~�L�O+�~��C7-�C�B����hCw-0���n�����.O��eP�j����]����ad^o��v��Q�}��6	4w2��Q=w���1��!�������m�Y
�d�m
B��0�m�2m�
��Wy�Px��Yw��F[=�3��Fh�;<�������h^+%�[!��Cv�#!v����`�����v��6�i��	
v�!��&��5xxN�-��lu����{�.�W
y[eeBf��f����2Xi��Cl�
�����`��v�!m�#!v��5�I�7�w�����7�\���-��[�v<�	zn�n�p�����6m�������w-!�v�C-�B��Cl
!���;l�
�����w~���Y5V�\��z:��fd��l(7S�$��W�dZ���k���|���#$�Hm�`���c!��)
�����CHf��a[�������������c�=4�m����&�z�m�
��M�2v��
��w�C��>�i�h	n�����+��Hk�|�&�|�|�_�,�]�w�a@��Y]��;�F^^b1+�Cg�	j���W�!K�Lx:w�b���^��%xI��-��U�n�a�nxA��*��������'eM�J����=�0�{(��b�A�[��o2�CC���C�n3J~�<R��v�A�qr:M���*������R1��_/xf��F���}�<2���������_/{�3g��C���������T���;���}2�o���Q-Uf��������m��������v����
'�	K`h���c��{$������.�*�dr�a�RA\�r>���XEC\o��H�[6��F��Q��Zm�2�]�����s$X[�������o���$��<�wu�������6[BN�Y
K����.T�=����u�s)MKX�:�������^G���hU������^"�Q��Kn���C�AsIW-\;=;�pX2uw1��]wDUx�u-��q�t�����>��5������Wm
��l�5��C7-&��`$+��C�o��/�0g�����C��U2
�D��P��B��a���Qa�����}�RA�8�!���-�`d��B��v�m��r������<�����{[��>����E��y������|������|��[)�lHn��d�m��m�7r��3n�$7n��f�����(xov��l3����%�SG���<�E�e�b�@�/���g���}�n�$+m��m�����n�n�H[��!��i�lP�B��WJY,���&��kiB����8a��xZ<)���������x�z���JC6��\�Hn�Y���m
B��
��![l+�w~<���f��7��6����0pg+��k����;z`��tR]�xK!v�C7-��v������m4�[�$1��H[v��[IHc��hT�c��������9C������??sX���VU����7�����l�����h����e��m�i
m���v���-&�gO�<�kR�0��}XpOP�;v���	�����y����'w�����K�A�fHCd3m�!���2����i)r��m�!w-0���	��Ow�y�9��~�v�����0�4x��`�e����N�g��8w���D���	
�a�3n�He�p��hl����v�O�q�>
)�|�`Rv���,��Y��Nb�N[3�<��q,���;�>�v�f��3m��}&��v�!w-!��M!���+��������iH7���.O��3k-p��(zo����f���>��k�����W�mc��Nqp���1<f�3����3u)��u,����;��n�����(���9I�c���\���b�h*������y��So��-�W�l��������<74��[@����
twv��0p�ml?�3����Q�k+*6/���D���v��U�����8
a-��h����,F�(���>�u��V>��p�������mh��A�N|><w/y��L� /~+�t<"��6���m��QE���nU���-^];�.��T-�v����X/7�[�l/f�Db9�S�HF�S�t��*!�W��2�>�i��J�{��c�	p=�x1�Y����63o��j�eW�hT�����X���zQ����C�����XoY>>q����O���9o"���;j,/�T5/[������Kv�Go"P$��)��8f�)�hj�����z�a������~�Hk����l�-�dd7m��\�B��D�6��.�� �p�������1�N�n���k���+G���i�N:�Eg)�*�����5��������������$6�Y
�i
v�i
m�He�a�3r���l0��I���n��|w-L�#�X�+�N�~�e�G����s�'kR������y����Hcm
!��M!����v�B�i4�[�FC6�R�>|���Z}�E�g/�/:���uDs1I���xU�j��|e'�������Hk��C6�n�$1�@�n���Cm�	
�l0��h@�����7����v�3��JZ��G�����<rT~z<�_(q��*|���[!�-&�v�B��B��H[���v�C-�	�i0�O2d��u����4�k�N�wN�j�0�����/=��;w}jH���z��m�����i�M!��ir��r��n���JCy�I����^�����M��f�>�;U+V*��o���%5=7O0_g]�����B��"Cw-!m�Cn[2Cm�d6�)��2�����{{��}�G�AII7���B���
����M�n��N�ym���~����>�c���"C[l�B�l�CnZa
m�H#R|��5'��������(���K��u�48�k2z�]u�P��l���S������I�����jA�d�1�i�3r��n�He�d��e�	����1wc�<�c����}��AgB��!k��<(S�����{��w��wq�������n�$2���r���l�����CV�B��� x\U��x+����.���'uKd�z��}y=vx���n�q���{:{&��:02��f�=X���x���/�Q������s�4����^������f��:��#Nk��� ��POC�v���;�q�������������$�����.8wT�_��'�jlm�����������FO���xd���o5@@y��uB���t�</!�S��R�^��~����vo����w\���U@�\����gu�WE@x]���
���(��e�Q[U}��$#h������(<(������#.GM����4^�Tv�)��:��]�jfV���+a�*�v+>Z1�������N��ZiK��do��0j�5�:�B���(J�������M2T�nA]���t���V��O,
��������M���R��H��3��
�q��f�N���&���*�8h���]�>j���;�^^�>�5��=������3pmM�^���I�|R��;aHf�����He�bC-�	�@��H����e4_K��/A���{Oh����c�@B�'k)�y������2���R�i���B��$.��Hcm�n�����
�t�\��}P��n�P�^NZdx�u��qy|�3�!��P���pX�HN)��������m�!���C7-�km�C7-��m��]�)�i�m�C���-<M��*���}{y(k��p�j��	�=z{h��Q��s�����[_U�hi
m��n��f�Y��!�v�C7-!��"C-�!������uX�7A���� �1�|����w+z��E���I�(>��5�@�r�!�������I�5�Hi�l2C6�H[���z]&��}�mF���*Y��c!��������r�I>�����>)>��m�HWn�6����L!���$3m
Cv�����!����>?.������A�/xs��O7�"�1�u:#�k.~������
!���;him��5��!m���h	m����A	�>s��dl{�
y���p��A(�s�
�ks�a��k�cq
GWr
���w�}��'�|!r?�H%���R��6C6�B��0�;h<.*�z��E���aa�������x
s'��WSLr���3�?Ty�=�������;i4�n[$��������v�!]�l��B���"C ���YXx->�U��u�.C��TCx]�%�!����SG\�9������}>����.����m��f���ZC[l6B�aH[�����4�%%E�g��[c������Y��5�F<�z��f|!�	����m�N�!Z6=���f���i�/�+"]���ws������+E����HV�r�K�z�B+[O���v�*(
/�o��jb�� �
����{��`����;��x��{Z��"!Es;�uQnz�$�A$�H?��qz�T���	$z��P�PjD���5v9�/����n���[	;t�[�`�����A�E�e�a�X�[�M<��=�7�2���:5�s9{��W.�GnA$�Q��x�I	������D�9��nr���b��{w�a�""}�s,��"/.�[���E�~������c"���c~��W�H����*L]Z�!Q	�#�+r�Z;�/t��1�>��q���a�����B��n��V������O�LYN�D���Z����y��m�:}��������?q�bdD ��{Y�����5hDH�?^��{�a(�!>��;|�����DV�]��DY
��a�>y�K��o�0I�Ls^�i��D�#�n}�nK��Q�\c^w{���y�����8����'VTr����de�����#{���5+�y]d�����h�l_��a������V��R�/&%!�i���YfU[�����n@��]r'�> �8]���
�"�"*7�����QE�����
��X�b m}c_1�k�� BAE/������*)Q���s�g!B��K�k����@-|�{��|���_�R���Y�
���T^���F>�D�m��dE!B	5�Q���Kn����]�N�5��f��N���[9�&-L>�;
gL��$]������}�m��Kz�~����s|�5��)�� 	���!J)Q���=�O��}��@E!IY������oh��5��}���o���YT@"��z�3���		
{��x�o���$��!E|��y�\����HB	��&i�!}~�:����������e]pN������D.������cpW�~�^G��H��Q�87�����x���qE����^N��l�W��f���	���{;����uu��$�@ �B!��w�l�E+�s9��1YT*
O�|w�c��Hor���wS�!/�����i�}�u�B*$B!_f����fz�P���X�����y9�c�����r������t�{�%��U�y9DG���2rG��0��54s�Zv�W������<��K���)���0��z-E��y���s��q�=/kP�J/�>��v��@D ��{�MoX�f���*G�������2��(�1�Ng��q��!H�	}��vr�����x����/��3����������G���tI�������#�6����ty��U��-��%Y��H"�d��B�u�g���?'/��}���b�� ��������9���H�2�������������NX��}z5P��������Q�$�.�$m_�`i��Kw��f��
����w��'h"\��&h(T)#;���EL����������w�����(HB���1S�q�����"��`�3wOZj�a��H�
��m9������x=){B�d��������h�T�#��[�}��H������?�Z|0P2;���3|k�^�o�Z�B%k��gP����y��Y�	H�����{�[��QV����|�Q�"z����Q�I�k��\��"(�1���?f�1����(>�?����W��;�f)}f��3Lf�yy�Ew������ �ry���/I&J�[L0��q�k�o_�z�����3�s��X�}G�Jun��M��.����o7|�l}���WzH�����������@�O�@$�=���:��D�o�~����H���9x���.� "$T�y�?q�B )3������""��
Asd��C�����oY�����U"�����K��S���z��_mvo���K��c�EE���+j��~c}��c�V�#����/��:��;W�T�i�Y���G�=�^���������	"������50�@	�����7�9�l��o?3����B�
NV��/����tP���z����*���W�����A	�1��?s�vN@��<o����2Q��S���z������^����Q�|������(y�����VA$��N��C�o�c���M�S/o�/4,�j.��>�B��"v�Z�f��c��2��X$���u�L�[�fyB�	�n���jD"�r�/��;�"����������������		�o{�B
(���}�=�N�A�HLn��s��U�T���b
���P t�	���M�����i��UT'������U�G S��2��r2u�i�3�=����A�e�4\��`����{��Q�I/hd�z�����@��W/cY��J"�>����f�}��)
H�������RBDP|�o{{���1�UV(������1x��Q$	�};��8��!
ox�c���{�DH�A-�
v:�M�������-9�3����s������,AF��~$���RTP����m�9V�+:L��@P�$�{�����s]�2��;|�n��b���g����I�~ �?���r����BX���rq��Y���B�
�7*���<�����QuKz�7A'�|>
�
%NB���M��������y�7������DQ1��~G������#/�(�
J���"E	@�U���}�5���g����g\�MLw�}���5�{>�@Qs����{����5���N���5�w2h��H	
�3nz���]�%�l���n6>�.�u����C����*��Q3fy�C��V#f2�������g�
��������v���zz��M�����,�<z� �TF#�y�No�����~��#����w���Y}�DB�����9��9!!S���}����v��s3)�E]�t�~���<AE���o6������`�(3�����W
1b�AB"��t���3��o�a�[U7!���,x�Q��_g��O��	 �i;&t;*Q������$C0f*"DQHQ)�s�jwI���y��V��q���Q�f��I?~��$M���u�s�b�|�;y�/X�9PU)B�
{/o1�L��n]�g��4������� (���C�����NbP#&Z#!s��-��s:��}�"������v�����g*�u�{o"8�f��}VB"ET �L��x�w����ss;��3�s������� �P@�5y���q�g
q�b{}��������������v����H�����wm��k�v2�]���:��vm^>-.�cl�6��E�c���hs��S�����^�99�����:!����
��"�R�%���~�N��r�f���
(�b�?}��L������u�����X����w�MYJ�y�3;��pBREg���S�w���D������5J@�o���r�BHHB
���cy�.�( ���kF�����_���;����z��0���;�K�r�2s���z�vM����??@$�A��^�#��L
1Yn�;���������)�J@���[O�UtF�P�/6��W�Z[GtS��DDT�RBB�#�[�k��9�B�e�S%uk�rWZ�Y�@}@���	��*�����#��[\����x�5�������"@�!D��!/����*��U�{aN��| H� �(���=;�o}��9��Sm���������[�B�$")	#���9%KUv��q����){�l�����PE$@�P)����������\�(�z�gBH$��B)!ED���5}��3�L���������+'^�J���)tt�A<U��W}�!+��.��	�����������k���gJ�I�����z2������YgF��9C'\������ B�����g����g��D��H:��������8���y�Q���">�z����1�>�s���bBB�%�X����
"��x�\��(�G9z�������DB!��ED69�o���{��k\
��e=����x�P�A��������z����L{�'{v����y����}��PEQDEr���]9��{����i��$(!A��8�}����o��kW���s�c�;5r��HT� ���S�����Ha��1tw�A�H�I�����S�����G������w�Y��!) Nw�7�7���Ne�
�����[SrRE�!@�=;i��w����c�q���	��=�t;��~'���D(";�&��r���jg���m)j3��u_������(%���o��]���	��a���D`���:����K�R(�n�x������rj���iT2�]��
L����������n?���|���({�ca�W/�����~�����Pd	���&G\�L����6�~?~�$g�i��;����D�5����n���~�~�� @H���������Q���*���9�k��w<��b�\
B�B��}�w����K�z�5�;�����$AATK�3�o����SGvst����N�$A$�A$DB^9}��5������O9��k��kLs�q\�)D�"no,>�5:���t����/s^�	$���PU"&���y������bju5�{�c~i�kzr
Q(� ����O����d���>�o3�"�T�,IDBDD�'u���g�^�=�_��~��9���
3}o�����,P`�
P���b�
�rI������}�g�b�����EPTAB5�TszK���U�M[����o����+b*(���<���^�q���z���h�Z���E_�����,8���b�t�jy��������h�F��=�,R�����>����u���5eM�8c�����+��VN�~T.�N�TcN�|w��}�h�X�z��j��1���P�����~����7X�c(!	���g�g��#��w���c{�	��9�B
�KZY;���Z3r\)������{��ys��#T�]����u�k�&T'�����)��	�x���hv�y��6�<�N��>$�> ������9�f3|�F����
DT�A7�9�������T����	�B�P��~$	�~?> �W+��m�1V=1uw������Q)
"3�gz�t�\�����u��>�=Z���������(��P�On�9y�D��0���6]�\W�fP�(K��5�3��3[��_�����/OJN#b+N���������[j��J���$��?�P�����-+z�uj�6��/���Y��r`�#;��,*�/q��vg]x�m�Z��&�v���,������f�Ub�>��&���$0��'$��7TPP	V��'ccj���(�	��z�f��)
F�8���e���{���R���_g����E(����g��w���P"�w�����}���R�	f�3n�
0E��\����7��� �TU)�����������W�N���{>�s��.�B
(T@��i^�L]F�b���]�qs�5���&4�P����5�����}���;0>,'y�(���E�S����I��)@.��b������;����_=xi�$EP����e*8^1��un�X�������)TB����Mo���w/����w���<m��{���f�z��"B��(�1��Z�����"2�E���*�Q(�BM_w�s���C�[����mi'��0���R"�	)P�L����q�1y�9�]���v�s:�E)"��
�40����r)�9���������i�GE��UVX���&�����)�S�s{�l�������I�8��P��������H5��c#{����b��}��~�!��R�]�w����E A���rcS}����kh"�&�}=�krl�PI�����_0H�"*z���yDED�B=������p�"�"!#W�uBA $�C�7��h4�9H(�*�PD��o����5���2���pnu�� ��7>|�B��T�(��7�z^u�kw{������1�������z$�B%"(>�2��1�Y�j9/Nv(6�����[���	H�����wo8�]�WJz�] ��x�$���
�q�/�z�:os����7�:�^�w���
�E%(�X�/��b�k;�����e�����l H�A������������������s]���=�������Vv@'���@��BP�
w���y�����{{��������w��\DP@��l)���V���<�����7����A!6�g����Ef���w��k����&�F|$�R�L(��\�/����{p�u�'=��{��8lY��%���	���Q�)uG���T�*��*�6����E�*~��5;
"�R�D���ZDDR�(�������^z��"(���}��b�ET����;�O�~������3��T� �P��>���a�i�!HB)I�=�^f�[�Q���H]�������O�����@�) ���7��\�Ly��y@�?}[mn�����������n5z�����<n��L9I^N<
��9;����j|�*���b�ht�:��$��vVm���!�Z�0T�)'�D��&�m�������\up��_LO�#�'n,:��}�{2�������w*��
5���;l������q��_L�]bN��e.��AlkUCo�3CN��2n��=��O���R�4��o9W6�����u�"��.�a�^\�b�[���g�/���Iu���x-.\%S���h�epWX����<�N�j��^VL��{���Vs�]��@���a�}��f��,�@ZpbB�+nAS���P�Zu#!����W�dm�I.<l��%������cmc��f�����<sU8��;NL���R/�c� �-��l���C}eZ���@#xy��n�)�}�T-���j����
N�!+�\�A�
4OL��u�.s�JV5f��=��"}�i�2u��y�H[�0u9�{xe�Y���DP3F����dR�pv������B>���n�[���p���+Ud�������V����N��j�I:�!���WI�G0/f�4���������/�L,,���dP�^C&k�&�Un�WV`V)eYH���o���d���n��4�����x���4�1#z#Kn�v9Sc��������L�� gpo���,6x���n_a��J���Z��f������@v���k6�HT���,�������*r����_A�iJk=��+:gWZ�r��K��5�����w���U�oi��L*������f�U+�]���������|�n6iXi�f�����,�4�s��+o�]UI`�9�BP�Nt[#���t���E��je��KY�oL�����=�>��d�M����������^rt06�������c*�n�e�n����t���^�L�YV4���i\��VQ�6�J0nL�K>��70�,�w�&c
�sY�2�/s����]l�Hc3��c.�}.��`�����Z�1s �W�PN�Mlt�u�\�k��RAu\�V��N��)�R�������N�V���\9;�y+��-OPj�wu'��_o4`�c����s%���k�ZsX���abmigq]��_Mbn9���!=�N=�l�V�����-��q��o����[���3��R���y�k"�d������e���)�}�������1G6��v-�a��M���4f�a
��sUwrw���b������PN�����
�kj�q{F0���z��)�qQ�����/�Bp\g_
y��X�2�s�.q�i�����@��d����������F>*+��gT��,T�h�
��������b�/�+\�u��9j���:w����
�o^���-���]p��d�X��+��3U�+P�A��Y���
�Q���#����m�nL}tI�1��0FI�e�u��&��evkZ��d.��`$�U���c3&c?�-}Q��k�E\fJn���t�W�$��B�2����a��!�N����J]��X(!�j�bR
�Z�����(�.��;�q�k:2D�!5����^^_��Wk�\�F��fC)L��,*�O�e�#n���-BF6�����\�g$��V�)w��^�q����=G(�
R�� �FR&�`+M����RS/��n��������5i>�X�]ha��[��������6�e0^�u����^���%�ut���WJ����mV��O�P�H�I]	Z�������e0�����;}�V�����q*�{)V����Z,KPR��%��E���Y�ou��.fh*�\�(�{8�<�=/���������_E"��r�v=k��jH��qW��R[��c�Y|�	�����"n�L��}��R�n�;�t�n��:1�����:.�t
n���5��4���$����������o����6��'bJ���DB�-�t���J%E��WS��`�u,�PBF������p��33��;|�M�4$����E5T!h[��mn�f�J��8^��^��J����.��h���V9>��n]1�wWK���qCW�\�m[�bbd���Tg+����{�lc����1}.�����x'��x�jXU��������u{!��u�^O,�'�
Z^vq���W$��Hz��K�-H����u��]~w~��yi��[���]���������9����1�Q6�6�e��3��4���][��,�t���8��hf����;C�C����Tk���W��M����<wx�M�*����~gT�������	���d������K�F2����t�mV�yW�s����������sU����5������|w�|���������>�l���F��6�,\���������C}����gq�;LG@�@��a����j^�KB@�t,�7|3���b���v���rnwZ}H ����leYvDu�9+oa�S�|�p[���^]��I��%�����-W�Bl&�r.X��J���P��Il��
w�
�^�s���?s�L!�v�C]�l�����v�C6��C]�i
v���>��r}�gs����������_qx���{��]oW���X*�#1fMD!C��z����' |R|��8�����!��6B��)
��B����q���=������?5W�)����:�7�n���3�\��-f�e������{��}w�av��-�I�-�d��l���i
�a���|$�|G�5 C��wqZ����[��M��uv�,/U`]g}�LV������v���������|�|�6���Ci�+m4��ZL!���$2�>����>�>|7�x9��^��J�3��S��p���a��W���|+�n����|�=��%!�-0���a���!�l��Hm�dHV�i
����&��������z�$^tD�h�Gj�p�j������ho/U���Cq�m���������!�-2B�Id-�)���lr�0��l�m%����?x��-���
l3�;�j&��L�Q�����n��8-~��#$��d�n�$+m�!�hY
�IHf��H[v�C]�C����W���s*w%(�����]]���F�[����^���h�b���n��}�nO��-�`���dd3m�5�I�3n�d.��\��n�s����}0g�	w���{)��1����avq�=!b�\�	����yf����\��n�!�v������lHc��������Cd.�JC����o�#��h�N�5��U2�2��%��g��������Y�8.���!�e���5'��3n���m���v�B�l�C6�
���m�R���m���x����ry\��5t�������G��/���U� �xm�.�[����[B�s~3�jT���������_�U�}�!��@��L-�Y�`�D'��F4�39�,�:L��q��K��r����X�9�7�B�X����r�lG`����J������y#{��u,�b�XX��Eogz��Q��ZV%)���F =��}�*;:9����e�{�'Tcx;j�#�.�����t���S�g�W~���K>��,U�$r����5���e��;��KW�3�c���	X�Mi�p�[X}�]+2:�7d��?yzBA��8U��������BQm_fI�3�����4[��r��X_�@v������v�����ie��}7~�n	�V��m
��
����>��<F��� :m-<r�F�|"4U���ba��!�v��*�uv�S4�F,�������f�[���p����h�+j��mG��R�O��'���_�1�e��c��t�e!�v�-�A![hB��>������������Vv=R��}:��a���������w��|�>�����K�o}�����!������+���1�M!��i
������>��|/�������{�
\~{�:iT��7�K������r�����������|�$2�R���7r�a�l�!m�Hf���r�!��H'������u������5�:��eC.�:���3�Q�sb���]��1r�n������B���
�Hn��$2��[��!�iH~�����r��nV�}I) �.��sj�K���7��Xt�R�-�����!�e�
!������Ckm6B�IHn��$.���h$>�y��~�<>}�YVGZ�BN]op���^�z��Rk�l
�A����]���p�n�>
)>��yl2Cv�R��B�����%!�v�!����l����=?~_|�k]NW,�eq����]���PyM����?w����}���H[v��n�K!m���hRr�a
�l�CHVH|#�����=v
�K�x��������z��NOA�A����0����3�����
Chl�m��-�c!v�)
���r�d�[e!��4|R!��h##�K�/
�70U���#t
N����@�[�S]�h<9y��i0����6���h$+���+���7n�He�dHcm���.�W_S��)����]�e�Iz��c�\$�����J�b�z���e]���� ��l0�����cm�Cm��[l�.��!]�l��e!�����:��ow��v�#N������|jh[���u�����k/�h��R������\^+��k�����{���:�Q{��+-���n�[�����&�.������� ��!�L��tt������[$Q%����c8ZG�V��������`�Y6��V���g^V�����=�A�{�x=K^���q{���s1!�fw����y�Sm��;��L�WL����:����~�l=�v$���$��|>�{4����Q�����/����U����
z(;�����o�M���o����R��������s�����������[�����d��f)�c���7KMr�f}�u�:�7�	���1��9�CX�s����WLl���3����g�F��oI�9��K�����~��X�t���i��WA_�+�G�,�|V���Sr�e%��eh���zo�~��X,c�z�)�yd�uu
Y+��8:��(��@�]
���C����m�a�1�a�.��CnZB��Hf���jC[l4�v�O
���6W����u8.�'�����r���W`��
�2�u���L�}�d�zRn�$3r��r�a�ld2�
C7-&Hf�)r�$.���N�>^&3s�/3\*���{�EQ��������0lV�����QW~�H[i,��IH]�L!w-0�[�	m���n�m�����<������Z�"=�9��:�/���u��w���/��
��oc����|�|�#$��n���$7m��6�i��|��G �������d{��Z��L�y�������t����W����f��}�v�d��U0r�V���������I,�m���l4����n�r���#$��r| �:^c�^.���7��V��k9R������dSi�����&�����u��~�����}�r@ �+��B���l0�m�HV��d�>
�>|==�S���G�������93�=ic�v�y\�1������s�������~������l2C-��-�L���He�H�n��[��$�O����}�������f<N.s��e,V[�����YH��)�m����������������v����Hn����h$5�f����!�l �G��\�W�<'���-�m����r,O�w��u/+Y��5����;���������5������n���i4�m�D���$5�f��v����� xgn��d�V��X��R����|+-��/{HRU?eIJ�U���_������H]�D�m�l�m��1�����Rr��+��Hkm4���ST��V�{9W0��Ax�p[7{5���F��vdOU+�xrE���C)UN�����Wvq�n�9���\4�Z�%�_V�X��vY�������5�=���h�<��`�����'S��������y�#60�JGosf�#s]C:�K{�����>���Q�`{�'��G��o95{���v3s���k�w�6$���ev^Eo.�v�f�~���s��Lu���
���N��a��<%O�����:l�4�2'p9P���I{�:��js�����
1C�w&����1�3FI\��c-��vA�n�t��{��T�q��wX��6^�6�8��;u���.!�9<FI�\����%_rsP�Di\��5��k���B���cXI��;�o�^�x��g���y��vn���l^w	�_�M�}��,:3��������&����ngPL�[����p��t���7^I��|����u�Y����u�����n��A�y�1���.���>��|!r6B��!�t#�JC��������_�.���xS{�BkpMh���7����q<��%1��vzf	�C�=��D��������r���l����v���m���m�B�[0�8�C�wJ�����;S.�T��T]qV}�s�=�d��5�+�
Y�/szH[l)��D�;l��h	�i�lr��m)
\�1�|.�e�K�/r��p;��E������5����v��l��!�����!��0��h�n���6C7f��vCw6$+�|�?u����$�����@�����~�A����0������z	�h$1��!�v�!�-��CHf��a
��Cv���)|'�m��7
�����|���������j���^v�f,���^}e�bB��d.�)
�l0���!v��nm��6��
�hC�=~�{���/�<��X�&�_3�H��-�T��x���F���p�}Y������hi�i�2���1�I�m�!m����$-�L!�����~���T����������r�M>���(��PRG.����!�R8�"����i!��#!�iHn��0�v����'��j@��o�������n�H*�����u�8����N�:��J��O�N����|�2���.O���B����ld3m��������'�|R}�n�O!m����wj�n.��&���������|c�������x}����2��!m�$6��d-����a�l���e�`G����|�zu�t�N��Q=v��N����,{��y�6_�R�{��)]�$�j��s�x���z�@���f�%&vE��ov���`2\E�p�����>"���D��`����n��{�H��g�,��6r%��&0
���C8�{$�<�^V`a�b/|>���f�{
�P�]�9������=�����=/���h92{��J������������D�!���I���,���x|�H�E9�{��z��z�6��o��������o�dv�SV�+���6�>;�����������b�5�{��K�Ab5}z\�U�z��QTT)Vb�R[�S/i�gt�X]��`���]j�����E'�)��Z�k}��X����7�!]����=i�����Lb��17���l7�P�����>�����������"��U*���n?�Lji]�,�u�d:����1���U��M��!���w�v�x��W�3���7r�0��������c!�iHm�C[v���l�9�����5�����eb���r_uT���0E6��3���^�z�#���/�H�1����l���H[v��\�>G>|R|��U��r�����#���^E"{�H�/q�M�~2�/�����_n_H����[�����!m�	�i�Id7m�![hc��p���tp����c�����S�y,���~�z���}B)<�������������$3n�2�Rr�d������&����]�Cf��!w-�H�|�	���#kn�/lu����%���x��x�nz�J�va��_:�h��|��y����7n�$+m�r�c��
v��m��v�ir�!�w~�8����Wx��wsc/��RWK�@1�`+*j�����{���W��-�H]�����Cw-���`��vCw6��6��$7n����y�g�C=J��{�A�%R�]��}��4�~�s6r�}�w=���qVX#��rD�6�!]�!�l�5�f���&���C]�4���C$7������G��K^xv��������\��xm{^*Jc�Z�^���P��b�� �2��2������]��7r�Cm�$6�d7v��n�Hyy�������f\���K�E�c��iz:�v�^wh�����Y������A��������m�!�����am��
�}� |6���c��������9v�=�������K�1j���R�Q�a�����R������>
)C��s���m�!�hR�a�6��Wm���.O�c�9r�3���yT�����*�9]�[t�S������)�H������{�<:2$�Z�7�������1��V	]�����o8��A7NhK^gQQ���m[��J�s�	��t-���������y.:b���������ul�;�����D1:��Y�nZ��Uo�x]�v>��+���F_d�Fi�+����K9�v��A����(m�j�;��������={���������o�2V��lV\�:��t����P����
�����J��=,CY�D^�W{�Q�h���+q�16`�������04���%X����]a���rl���jZ9�RU��&s6�h�;>j/�}Pf�}#�i^K�]��<�|��B��j�O�{�P�����+7����7��j�]�@��~M0�g1��4x�yG�d������]����}1fQ�%X<t�]7������\tn�y`|+D�2{7tK8�k���U��r��5^3g?_X|#R��m�R�f��-!����[&He�Ym���v�!��~�����=����:W-����������z�I�j75���o������|R}����
�IHcm�!m�R��l�[������V���m��m����K�,W���q\���{�������--�������Rn�n����Cm��m%!�v��[������OX�u�u{�:-�*���$�p���{���Pz���VLn�q�����u7a�5������-�fHf��m�
�B�e�Cv��9���������x.����0��%Y�@e�as��n��e�{T?f��O�G����A����!�v����Cw-Z�C�������'�!���`E{}��H���5)��d���S����������;y����~�����C.ZCn[!���Cw-��Hi�l0�;`�����	yO�����gf%r�������:n3t�Xq5}�U�)Y��}�f�p{w9��B���A!��#!�i)
�i�7r�a�h����MI��|$R������Y���^���I�{��}[�r9=&;l�5��,\�I\|y���w��=����B��d�[ad.�!�������He�I���I������
!X���K�WY�f0����Z���.(�fu5n��-�bC-���l0�\�0��h$2��]�L!m�D�m�!�p������z��������5����4�;M*�c�c/|�@�	$|���6��H]�L!�vCwv�!v����t���r��>�:������U����^�����0�.����������>��tu�xX�YT�}RR~���P���y9.�<@��j��j���sVu������JSV9���{D��<�n0T������0E��ep���Q��d�)^����|v�"�l����*�Ls�Hy
�s��������w;��b���J*,����c�P"��W���d>���3����v���0'��sD�V��u�f�����=z��I����N���-�����N=��"#o���wman���V�e�(I�VQ�I�p�Q����5�����}� ]yY�s������"~���et�~����;l�;:������;�g��F`�U
-nn'�u�)�M��y����������%�'��[Tm�*�[������wb����������bv��1��P��w"���F>���8#�<WP��]�v8���J���4M��U%�{���#���>�M���R��H[��[��6�C���>
G>|��+��kkk*7��qD��	)��9n��1'T���&p�l������F�P����z��;l)�l�I�.����!n[0�6�i�h�~���y���mf{���=�!e�����T�����w�����O.9��!����v�!��B��
�JC�
!�v�A8����rA�y]��t��~O�7K�+8������[�6|-)�,�_�JCi6C-���d36����FC6��m�Cm��[hRw8w����5�����S������<�m
�s�U��!o�Z�f����?��!m�����m,��d�-�H���bB��i
��C���n���{�{����S�������yb�^=����s����{��r�a
v�i
�f���&�n���l����� |!�|�v�o��W�����
�N�����n������3������>���L<-�s$3m�!�v�!��)
����CHm�Y
���m�x]]�c��r���qUF�e-�"1PlM���U<�R�����^�]�"��zs�y��e�bC�
!�v�C-��lH]�`�e�R���������DD2�b:�D�a�����$�����d���{!14���p��{��y��$.�%!��
��hR�l��ha
�a�+m��B��v�<8�2HU��1q�����\���:�����2`����\��}�����d6�%!�v�!�v����l�������m�i�l0���q����2����X������y~�y],S�M���
$���6�LF�o�r�w�e@������9����;���74g<0�R�O|=Pwj:i�1�������ue��4z���ko;M���Ht�:0T�<��Dy�������j0=��tu���xx�B�6�XS�8B���eM��&�Y\���P����xZ�ek""r$�8�������@j������������\2�tr�������%��������X%���^'�BJ��n���pk���D'1��t;��J�]��Je�G�����M����=��+GJ������f.���o���� �6*���h�))N�B�Ux��nI����W��l5�%�;�����`���fk\m�:���/����g"�V����S$����B��z
��b���{�X��F��<5��l�^C<����\���mM;�X���tfCMQTRt1%r����0Z5�������m�n�Hf���3r��6����-�Cv��B�����C���.�/v����j�c �}����!`~�g�=�K{���?@��I ����4���i
v����Hk��
�a�7r�����s����>�~��������fv;��@=j�
���%���Wq��j*���_��"��1�fHM!���c����v�����B� c�$p|~�9'���?���/
����Rl-������W��2w��Q
�t\����PY�F���r^��>�$��ln�HWm
!�v�Cw-�m�!�v�!���C����_q�<���J�7�u2B%2�a��^��Y�u0��M�}�����6�r���@��l
����-�r�am����;�����D���U��uSx[����]q���a��{�����y��Y��CnZCnZL!���C.[0�nZ!�l��U'�/�	�>��ojf�����nj������aIgS����Om]n���}��ep}|5xg��6B�IHcm��f������Cc��Cw-�V��c�����+|v�n���F���h36;�7K8q��#���3��7E*6��rL!v�"C]�i
�Hi
�ld2�JC6�Cm���i)
�J���y�30q>�3Ou�N��_��7��u�/���XUs:���r\�������~�r��
��!�i)
��Hk��H]�L����7r�!���!��"�]j������!<lc����!.�����k|9u��v7Q���|�?xR��HWm&��v�!�-&���V�4�v�i
�dN��1GNM��$��������%u���wq�����������)�w)��D!������IV.�'@5���.���P�^{�9k�L�q�x6g��f;X
����@Z����72������*�������G�����S��k���V��>�j����pU���WQ)e�2������B�����o�K�eY�\w/��a���G8xMt�nS�\<<'"�D��a�<�Ss q1�\��xwvn���5������.U�~��R�L�=PfZ��:�d�T<��H�#h���4me���|rue[,��6��g5^���:�N�����t6�n�H���L�������^
����F]���,r�����^��+��N�Hh>�p���}��
������&�
��k�\{�"��.��f�e��BNh!d`6���4l��w]0���"*rF9w8���e�����V �n���Q����Y�1�����D~�n����m��I�m���l0�m������lHe�a�3��������p�(�f�����:��D]6�������;�����>��1��H]�L��l�-�I�-�`���M![hi�l2C����|~�^>�7�<�o
�'s�h�Xk�`����-
�n�Nmev^^��O�z|�-)
��L!�v�Cw-!��C[n�e����)
��!��{s����Y�.Rq8�������^z<������q	5s=��oSx�{�+�����f�)
�M!�-�!���7n�!�v���i4�;i4��7�}�z��v��ce��A��,x"�%�n N�J�����d�7O=�sw�#��-�C7-!�v�!��4��ZL!m�$.��$2���?w�����;�����e����t�CO��}I�s�+�|wg.k�������|����i)v�i�i��
Cw-��iHk�����?�~��?�;���
�7�������>Yt�t]X�<�����\��2C6�)�h�e�,��JCn[2Cm��6��
�`i�U7c�G�9�O��I��<���#W�n�'i]���rj;I4��4u�|R������7r�2C6�He�Cc��d2�JC�6B�HS��;��NV���Q�:��R��L�a)�/B��-/v��I,7�|*���O>
B���
����v�!�����B���f��6C6�����!��������~�SZ�P���)�2�������d �b�E����+4���>n����Ccm��7n���iH[��!������C-���g��j��W��@�w�1OW�>��-1{�xm�s��2f���k�k+�����x��sRw�K��k�3>+�49q>���C�:�(�����o�1�@�4���&a`W6��E�~�sR����4���/z�.OlW[���;�5�*�9,xxmc��X�
D���^���`W�W�q��t�{�"��o��xF����6�9k��
���/5q]�{�o%�[�(��Ubf������r��.���NK���t�Ks���;������e��-i�Vu��`-���3�ud��=��)N�]���)�tE9����-�y�n��)�d�j��i(�; ��s�m�������G��|�a�-�=���>�z��6�*�"�Qz@^M�V�xP;���=��?M�;�g����Gsm
y��4Q��n�M�k�g���
��zQ��|��rR�F���2�m�mYL	����u�4����=�F��|^{�T�4^�����0�M�_|���6�Cv�Y
��C.[�3n�Hf��$.��0�����������s3�F��yb�9c�E�PT��Ls[Y4�IFkB��o��
��_�.�������1�I�2���2�����c<.��c���� x_f	n:��d����Oy�o�(��y��W���iY�w�3��	v�in�Hm�@���$1�t����r�V�&�7|�s��w�y���7��qr�����;�q{N�<��o���y�&jo��
��iHkm
���,���������d3m��m�FB�IG�]��Ak��*�.�3Fu��Y�j���f�^�Q����\Q������1��Hm�L!w-!��"B��C6�R�I�5��!�-����������(Rw�����&F-�u��r����K�����������Cm�!��i
v���jC-�"B���
�B�f�R��|�?��w|#�opXFe�n�,�����',:��"m��(/v��8�
H��a�lHkm��]�)
���m�O�I���$������cU�N����{��4���.5�yrx�����m���i�N}��k��Hcm���Ci4�nZL!�-0�m����4�2@�H�����,E�.�x���:�X����\�l���������;;������M!�v�
v�Hm�f�m��m��+m���m��3n���.�q	+���w.m{q�E��+.��E�������T�D'��=�j����QV�!v�
�@�3r���Cd2�K!m��6�Hz7�o��3�?�~[�=?Y����|��K��%b�dNd��z��h���#V@�BJ��yOt�|'l`�P����0�

���t.z��6@�}C�C��{���~V����o����%&����}o���0EE�>�""**
o<��7��q�RD�c��p���=����|�D"$D�3�c������T$����|�}��}��DDF��������)�dT����;2�n�^��\�F���\�����|��Z}j����7Ss8�^Y�u�}}���j�}��e}�;�3�,�B��(��?��1B���������{7�q "/Nf�9x��*"#\��z���R�"R[��nk[���s��"�����}�k���Mr@���{X�98A�Q�������_���1
U�!�a9�����$![e�Y�����Y��_o�V���QT.
At�^l����-��g#57dY1�SG�t]�T���+��4�S���N�,miRU�I$HC��}�((@�����9�$ ��������s�� �+����c[�.(�H����f3����(�����1���]���#{�S�`�����>��r�]��&�ez��-De��"��n�`=
^=�\G��V��Yc2��4��U������9�u��=�s>��W����8r���{��|���H7����V���kg���������o<�\� *�������w3&a���	;}�=��5��"��B"�����+���$�<=���1�m ���{�&��[TJREB%@&����wi!D�k��t�����������2�<z�1(�~���*��wTb��^1zqW�&�s�7��(�
��nxY���J/�N��Q!+�+}��Qyf�U#���N�S��^x��7�~�f�AA#��sy��5*�!�3z���-z}����@��O��K��������w����!�N��3�5�c��BQ��9g�����Q>�1�&��X��q���iyg�~��|��}����������O�ZW����D������D^K{^����C-�S�{�:��9�N���Ssk�����ea�2����^2f�7��4JRB��.������T@�C�����}�q�k}���)X�c��DA��q�8�}�7�
�Q@�`�r�w�����-������A
�L���y�h����J���� �%���;����`����X9!�Xwz�=�����Do�r����Ug?Z���W�R��,d���1���t������T����j�x������v�N��o<�8i����bEg�5��v����	�a��k?+HBF�r1��)/����o{������o|���i�0�Q	���s�����*=������	 �� ��~+�T��z�v���3|��C����eE0�����\s����������Y)�����������}vH��hM����Gf��������/(������x�%w�W�}�/���������b���q�a!QB�"+������$R$F�mM�}^���h���_��
g7�D)DE5������3�����!DOox^C$UE�������H���7/�}�Ol�O�OvV4t��������e�C�\�]��o!&-�f�W��$L���O9������Z�C�9�e,��K���������'�}j��u��Zh�9��I$��"����{Y�A)�wW�����k�$ !/������(��F~���@*�"(����}}�f����"���1}���3
�(P����I$�r�{l�v����r���^�-���|*kS��	��#Gr�|�F�]��x:���h'@�����e/���4����\���p�Y�e������,�{���R�~�_k�<�����)"	����|�=x�������g���d�H�}���e���U_��vrn!o����1H����1�,�]�Y�B"��A��W^���b?��F��;{Ti�Z������*��}�����6$W]5kw�D�C;�^B�{������[����_c��6���-�g�[|�w���;-w�^�F��.�9=����}z��c}�����D��(���������TE7>��w�<��E"�Rc�c���1���D�"�_}�w�_��g��Tc*f?����'���{��g�1Qg3n_w��Eo6v���Jy�cE<�kyJo/�k���o�p=�����I���?~R��.bR3���v�7���]z�B��ge�����]~�x�����zj��'�^nu��?os������� �B"=��__��w\��*!T�W���r�'"
E
"�����3sHAA�Oa�`B5��<�7�4�PR��;����RbD	���Z~�����a��f�b\�)�Hgx�{��Gv'�T�[�����Zl�������w+�0ey����P���,��G�|���/������������7���EF�s����B�'�{���(�"�s���ccAPS�w{���,�Q	L��{w�o]�����JDI�{;���BQ�5��Q�� �� ����u�[����P�Q�����W#V�[�2��\��h��R$$HH�=��W�J���1��\��[�2��XbR��B���n�������q�OD�9�{����q�@%%"���
P�2>e2�8���_v���O_���"�
��{Y�3=��������9�s�c����
�$,����j1J�n ��g�@�I ��B����}���f��52����l<�ke��b�*)T�A	������gS=z��6���j�����bQHR�H@��T���������S%�g�AF"1���")���������i�A����^�����V{��b1�(.�(~a{Q�}�c�qM�Q�A�]7����-}k�8e<7����~��U\#�����{a��d>^�
P�������?_Q���337�
��=�>�q4�E�����X"�~i��s��Ef'���Uj*"��Y���w�����B	���&�� *) �����>�6�������c���{��q|�6����������o����������5����j�d(E@AV��+��W���<����������;������r�=�k:�9����;��� �$Q!��eZ:�b����zX�zV����>�QQ�""';z`�����
�js��Q��.������y��A#�T@�R������c�oe�������{�c�����8�(�$@�Q�/&u�s|��y��wW���{��f��c����k*(E&u���n��=���.s��zs��!!�Q"B&=��z5�v�3����:�u6����F�����=�,��,�\TF5�qG�.N�]�j�\�!T�/�����TofD����xs����%^c�V.p��e�M��9�����) ����{��=��%������Z��>�}H��8������`��ow|�:(�"�c�s/JDR !"��11 ��Bw������uV�PB:�������T$DD�'�p�%�}�*5�M��5{Oc�$E("BE+�V�/7�w���p���;�o��X���JAA>I����!�WV�
c�i��X6��eV���AE�(��N��]��h���Q'	�N�o�H�QHA=�����u�c�L���sy�gw�7s��1���EH�DR� ��0�����
Z�K�����%��� DHU(L�s}8��w�m���|y��~{�������A0}C�N���R��x
��*��$��������Gs��aE!J) E�tx�;dv�Rc�v#(��U�
�(��K�9-�nk�����W�s7w{B��W�E�sk4��Od�E��
�R�����d��Q�����^�B^��^�xf��I��������15X��\������EJ�{�q�.s{�K�Q	���9��7���)H�A�ksx���s%��o�f�Eb!��|��{��1c~=��<���H>���!��"*����;7p�1��T!B"R������~��s8_���>w����DUX�$��%
^S�t�T���;UF�G��n)�������o^��=�/z�I���������%�����DA"�RI�gx�����9��:��^5����P�DN�<����]��i�k�9~�D�@�
���V�Z��Vm�/`\w�>��|���<�����f�
���X��@���$��|]���N��V��sM4��s�����t���{�����v��Y���b���
��W$k��q@�H������,������H�!l^����|�7�^��K�"���[.�T���"�DC�(C%-�m�/�(V�CE$�Z>�&��'�����op��������b�e1���zs�����}�{hDP���~�y���V,^\�w7�����E'���b��PIm{�y���$�y�����(PHADY���^3�q�|��JHB+[�3���
�Y�~���g�w����PF+0}����}��<3�|5��~����u�o[��T*R���@�&W:�%8������}�"�(�{x{�{�{�s�e�D���o��u�c��H�QJ�
��������)��K8�;.�\)�P���D+Y��������a|�9hm�����u��~?H�TAJ9�_q�y�nz������e����!�HG{W����b��N���&��x�@P�@R
E"#M��;����o���s�k�s�o]�b�H!	U�f�V<Y#yp�X
�[����7���vJ� �!JQ8���o���.|�P�w��h����4��-qy�HHc�:��A��7�z����%�x������o�������s}�������8\9au7u��g��b�%��yx�s�}����nv������^��������B*(���M��{�kE(�/�~�����=�� �t��/�|(U2�v�4f��LT"� �&u���o�{��BB�"}��/�k7��S7�
���6�g�/��@)@�EM�=�����m��n����X�X�H�� @R%k����������[���T*�>PP�	:N������wq��KB **!k����9�s1�]�}�p�_����T`�`��I$�p"���<��:�L�awMDFDU�F�����>��t���Gj��Gj�.s�����$�A$���Cr���2����oZ]������sy���B�/����9�M�s��qxL7|���H�$$,���vi�H;��]�.���)QEE �;�`���$��R�bUeN��`gHZ�a������I�����Rn�Rf.��TD�3)�28�}���,6�����N�l�2�g�b���l���/��@�BC��}���j�Z	AJ�3}bc���|�`
$��q�{��""��}�3��9��'���~;�A������s�Q��3����z�V1b����s�9�>�����R�EB���	��>���c��x��n������B"�W��������6�o���}�w�7�Gf) ��o��m�o���]�/p(����l)@,�H$�A �$I$8&�����WL)�t��v����<m�D1Db�����r��MU��n]`4�:��I$�A")�D
T���{^�bv��5����\�'y�j���"DET!JA������FY&������Q�{���"��""y��[���3'����[��Zp��nn;�q��(P�"!w��{v��s�y����7�����:A��B �	������nr�)�\����R�V�����]^?$�m���f��T��gT��������~��q/���Orwh*�W��KT�=�>�`�{%[<�w�����U��-4I$��$�A!�&&���HDB@M�X���*h�$(���8�{�H�JAB'���;� �!��y����K����������
 �_y���S��y����%i�yO=������ "�.���q/jffo]��9g�aA@�D���&o���$�xe��R��t2��\v�oN�Z"��Q�vM�>�gn�������`����!�A�HE$B@��wS;�{��wY�y�;�������m"�AEUQI���|�eBj�d���ky�$O��@�B%%�S��\��s7���5��������jr
H��H����J���.=�C9�N�:� ��$�����/Za��c����7|��M�X#D{�t
�o]��w��p�;1����|>(�QJ��Ovwz��}�{:k��3��������!T�������t��D��/n�{J?1O�tV�b��n"��N�9~���8]pvP�������
�w���;�K���	�2)P������� )�H��s�j`�Q��B@j���ST��B��7������$@���"��5���k������I(S~L{�N���
�R���s\���u����U ��*+R�w�h�D)��I$�I8�;7�Z�<��}��!*�Vk�B����}z�i]:\�(�\�Ns^�s��1�5V@Cz�]�0JXVd�j�����,�y������t�kO �{]S����,�| ��N�\�Z�,�W�Gt�y����U��=1��F�Bo�[���og�X��a��a�O�bG_���
i�b��0� X3y�-���������&
���Fgp���_*���L�������e-������!���\_NZn��[�yH+���������
��L:Oq��!v�ns�x^Vf�����,6����r��.m=�{��ic��x�����Nr�W��Z4���GFml����*4����4�� 4-m�}�2��X]�������rm�9p�������w\�aCe���.��	�]��N����X�zp�f
��)eL��6�i�vJw.��P������u\�i�^���4�:Pj^?���='B-}��g���r��wf��2�����s��lNyz.�ib�PY6�y��L���(�m�h�vH5}"� L�W��69�����Kx��Ui���-���h��o5����@�a�]�*�F�	��G�=��}��S�\�Y�*���
/���*"���O���4LK4^a���[�����R�(id�e
��C�1�)��h�l��t�8�������u����R�2��t#'-��|%ZS"�E+C���Z�v����������������2}a������kK�j����M}�"�&��l�B[u���������u�%��+�r��i-�ju�iU��@��z6��
�9�d�4j��qh�l����
N�o0�h���]�8���O���p�j��P�;��K�VMh����(8�1��M����;�)-�E7x*�D����3��;-�Rg��o]C������� E�Z�<
�55K/o�m-A�������1���9�!�s�+�P�g~y���5��oQ��7�M�\���a�\�AT���VA�:�a��x�6�X���5��n���:3R�������J�D�U��-��<�h\��*��Upy:��Uo!Ak|��*Z��S���%�@����$��v���_P���`�APR�[��X�K{_n�G��f��
{M��yj>sY���C;Y!�'N��()��(u�s�S���p���j��{&��Us#5���V��G�������u��)���S��b�WO�6��NH`�pY��F�T��Q�lY�^�[S;���0�X�]�+�����g��{b�}}�u�����FW]=�n��,�����q�s~�|X�v�Yq��u���v�u2�����9����b�k�$���\\s;+%����M��fgQ���Bs'�Jz��w��Ot���-�t�U\T�}.�I�v�kq*���]�:;����7u9��b�����:��������Kb]�
Xg-�p�4�p�������G��J�Y�zph��\s�������3�B�X��1`
>/��7��t�9�����@I���BB�c���9H��|I�������;l��&�-����l2�!auE;�����t7��H��[�E}k^7��k��k������
U�:d���-�:X�W�5 v��v���N7Y�
���"�C��E���P��RR���gNm�=yb��8���y�J�6��
�����w�ec�el�d8���>i�y�G(���wwP"]�Foo��*�CI�5�M�)��|4���/B�q&��������9�{wWu�����0+�\��(+7�(��b��$cx�=�Yz�����<���Qn�7�;�YX��e����9��Z�Es[N��rmj���uS��$9��f����HAN��y}�^
��;��o��;pJ9�Rn���k��ck�������:W���������H5�'�d�7z+\.+Y|V������a����RF���N�Y���=k��3:����aR��n���h��?�ot}�����e1g�������.N��O���9��a�b��#����Z��y����W����:3R����|������,=�����cM_����[~0�����X�+-����\�t��n3q��B�=!�YQH�@�s0M"��C^���='����}�z��
���S��������n�_���`1�M�(���3����h���b�+��7����9�w���������m��������I�}��]�vn�����nn0=���xf�/xxXi���lb�([�1�����UR��{����z%�+��@
o��1�{i�x�c��?,HL��5�i������m��#^$�P�0w��_l�@�)��0�]/�����wC���������8�e2��X�/6=�|���T�#��}G"uE(��Tk,oy5@�������O�=���� ^��&wF��fggiw�>jP�n�6�5.��=9;�F��"�'���tL����B�h�j��VC
��=Y]��O,.���K��\��1�o�w���]1[N��o��-��?s��n�,�[e!]��!v�)
��0�;hC.Z������+�~�k��z���Ie�c���M(�'-���u�Aw�`FMD*c6�=�Hn��Hkm4�\��n�2m�!�-�C-�FCm��2��H5[�^�{�7$�r:A��Oz���K���l���`>J�K�|>��7~�����f��V�C����H]�f��`��v���/}���~��h�Yyqv���������~���sup]��wW]gm�"����~�����m�,���R�l�i�6��H[��e�H���4�y������/wM���W��j���e����]���,�_]]s��z�c�s�~�9 B�[2B��Hm�`���C7-��j@�'���j�Q��������d�P��8}�
~�ov5�T�}9�~�<�|���B�����l��B�]�)
������[id2���r����y�/h��\�ww�u���`��2������_��E�S,�t�M!�-!v����1��Hf��n����6����i���r���Z�/�gY�����5G�Z����^������a�2��D�M�o�lH]�`���p��l�n�m,�m�!��
�����nZa�4U��jn3�5�Pz���8��o 
��It��Ss�
P���}��_Um��r�e�L!�v�!��H[��[���r��r�0����������fe*mh9��S�X���#Q_3�nl��kg����~�����k��H]�f��
!�-!w-&Hm��������)������L<�37���N��{�����#���_�t�tz����;����.�0r����X�������1M����R{�
��
����l_v�W��/�~�B�����o�E�u]����Q�������|�{���^�-L�W���7�/#a����;R��OZ���1�xY��t��v"��>������%���
�v�P����1�=��z�z�����������{�=���}����6p������9�}����Z���� ����=��DDP�	
������{m��;Sn��v�R]\W�m���wW�h��%�*�^��[�<��^BIO��H�����6���X��}��_!�J�Hs�+���������N���[��	f'zj��w&wM�=�����������_;n����;6�[������3,�z��%g&��I���v�w�����s_p�� ��HWj��[����C�����`�b�{��x}�O�I�����!�-��iHn��C.ZB������l0�{�8w�y���~��/=���w_m�V�����G��h������v�
�a�7n�Hm�D��l��i0�m��n��b@��x�������u�oq�����C�G�X7M��5�h��iWC�����m�D�\����V�����C-���HV�4���a
��������m��i�kB�p��/XL������y$z��Mjx���C]�4�\�C6��m��n[!]���n�%!����m6�����x�eu��y������I>�U�|c�v�bwT�[�#���>-�@��CH[��e�FCm��|r��� _7 ���}��L�|���^��Mi�7�
?(��d����7�
��7k���;�`
�>
�>�� �' � *I��9>9RM���}es��y1�vT�ufu��|�����a�z74+���]��u��M�
r|��[e��nm�m������������]�����
���k�{s��XUg�O�zeD���+V�����O����F��� 9����O��|<�{%="�c_T����"�j�F��SPP�e'��#�r��=��Iq���O�q��r@�}��@�����h��
��z��J��,�m���Q�cl���]��I9�\�k��n����5$�!�@��|c���	�I�@�qi�DY�
�N
�&�����y}9Q+A�n����������|����K}"��`~�[P6~���~�$M�W�2�����
�P��f���b�(�I3�+#�(���!V�����\����i�g�cGTc��V��P��7�����,c%F��L��]��ZitjCV�/{���0�{�yn������A{��\^O\���h�C�7��8�'c���e�m5������Px�*\����.��\X���_}^}FG���K��XI���t�{Q�����eS�W��k�]j��9�.}
�}��Y{��Z�6D�)�0����,�����z�8��x�����Lp1��/3^��N��n�t�z�5�R�������K�E��T�����}kk5Nw�?� ���:sZ9��(��V�h����!�+fG��a�9n��U�S;W/
O�D��'���AH�����j��G-�}s��Cd���tH�hNI�bz�ur������%�l��`m�Cv�.[`7m����q�C���������^;cY����9�jV\�[ZK/lh���@�F��y���l6�������r��l��N|r|�s����U/V[�U���E��s�X��A�g��I�`�yW�[�~����6�'#RO�rO��}$�|��5$���rg������&��3�+�����?uY��^�vG�8Q�W�[�P
I��>�rA�R@�|\��'�I>������Ma����w�d�gO_bj��U��o��}�����D�� �) �>)��-��NO�rA�rO�r}�k�a�C���t���u�*����|Us���L������+��pwN��h�' m����.[C6�m��i.��r����e������Y�wk�H�r��
V&�E�'l��xL�K��U�m6�L�l������-�v�-����2C=�z��/`����j���s�eC9�~D��KOs�^+B�����m����f[A��m�;��6�r@�O��>��V��MW��o�^������!v(�eq�^�8=�&�gg��G'�9m��q�N@r��|�����������{xd��#�YWM�'	�n[a���8	�V���o{@m��)�$��r��|T�I'�I�|+������m
���e�I���&LU�h����
y�:^����������KE.AgV�=������z�������]<x{]�R�C���]s4��)���������o�z���k!�y��'*:T_�G+�����,u����L�����9���
,uZ9(g#{0^k����2�[�z`�V����0:*b�w�J��z#DO'1���rotShQ���X��r�NDtFp���������9�|}�m�l���]��%�l���S��^�����`"�}�M��VY������a�����wX3M����,�m�p�.��^k�������
U���>��r���l	����H��K&��i
���f{[��jAR�������X<�3�]x���S���
����]��0XG��Wc}�.z�v3�uL>�HmGF����KR}���=51/
���P�K�+�����y]a��v(u��V���7-c��Zv��n{g�=�����
��z�$��	sm ��n[`��n[B��
�I�����	���i�{&u����(,����BvY�l?vu\�W�������j���a�m.��m�]�`9m�m�� ��'�*d�We(�q��G��l�k]Z��=� \G��>���N�R��<���A���h6�&�l�[Cl�|T����T���f.�����2��D��N��}���W�L����U�%N�����������7$�$��7'�� Q��RO�u�8>�������-�>�!�
�7=X��4�5����,�}��s��nO�r}����$���RO��4����a��[��,W}t_
��T���Q��^�s-7���u�)^���>�$���6��7'�9'���9.H*I���n/O�
�Tu�9��Q�v�\=qr-3��e�G�E���%�����I>��n|[d�m�k�hn��[����{}�]�Q�;��ml��w}���O&f��Lt������e���x�� I>*Hr|��7$�'��GG�-C�zL��7���q�PJ���S����/f�����|����'�) I>M�NA�rO�r@����z��nf���gk�	�����)�s��x��qa+>�`)$��'�� nI�rF����7'�����a��������<�����n��\��;����E	�,������4�]ul��������#����j7�6�x�h����MS����k�U8�:Dr���p%����fqcfT������C9�tZ��������.�C�9�����=��
�����K�^�"�F���B*�{�T+)bY����������}2���~*,I��0��^�!�kr���^����D����uP�t���f��U=���X�0��N�c�;Wo�@[��~�<o��}������4g}.��g���;��Lm��V���-$-}W��qv�v����-e��,��������r���p�v�h/29Pm�m�?g
�i�]9d?ux*k�}&jP������%�[�6�2�{�k� �xq	�{n�,�]��.S�+�~y����Lj+d�IH��C���6�nb2����;�M���������+�������;�,�k����;�G���u�NE$nm��m-���k��m�����Pt�������*������"��m{����m��^o��k���Tt>IA��f�d���m�����C3m37v�2!��L���h���
-���@�6�S�w2���7�u
�}7w`f[Csm2��sm�d����m$U��	�iB���j�����Kj%W�oL
��/������)��T�4��2�6��hn[dv�#��\��I%2���������@�(\��SwN��Qe� :���f���}���hnm���L�l���7-�v�?r�.���
^�S�����:���.����U}\.�Q��������=�_E$�'%�[h��F��sm#m��m2��R��]�+��9��;���nU+�y'��]��n�Q�u��)P�Kn�`-=~��F�H��.[`[mm�n�fn�R@*K�� �O=���p�N�By�����lQ�Y���Gv��i�j���n����f[a�������m����.��u�T'�m���H,Yk{
��y^x]��njA�����SZ�6�7m
���m�m������� �'���M�v��9�;}Yy/�t;����+��g
�gf���1%������������r�\��n�;m
��|��}�=��t�I��������m���_DExfakuo�G�?\��e�+hi]3��2���������a��<2�L���Z����p���U�o
Z����U�dl~�7[K+lS�i��Y���,������wp��d�i��{1B4������xx�:W�u�^��0<l>s��M��4aV����Z����<b���<)s�C���_�{�~�����i���m����[,u>|>v8�o�5�:6B���jz���9����p�}3S�H��'��4SoW���o����]1�iSKi��Yk���������{j���czT��_^��/�u�������Z����n�����G��=Y�z<o�'QfZ��EU���~�t!���`�C�~h���`<#�M?K�Lh�n���>�������88_pIT���U�j9P���5Mh��L���V^��AIGP���3k��C�������*�)TO������������A��X��L�iv�f��\��v�9m';�#�'�(�����u��+���f�V����8B���{pt.�a��nI����m�wm%��f�n���i�m#6���o�cVU"�%h� ��U��F`��]���!;JL�J�N��W�����Cwm���m�n�fm�wd9w���W�Y�����/	.YY}�A�U�Ti�����2;S���@��v���3-�m���2�����npt�s���a���wLv��'�T�v��o��D�V��h�t�D����e�.��r�G-���A�ne�7mi8@���}��{��nsU�t���Y9PWS�t���mX�9l��:���f[I���m�������9'�$`���0})�,v�����^Q�'l��M�������)2�8tVS��~��l���[m&��-���He�|��|r}�u���������y����IWG���S��m����lz�ol�����H��'�G'�IM���9 9>I>���{.x�=�k���og��a�wH���N�ua�i�����NI��I�rO����G'�I9>M�_u������z�h���]�R���j�mxi�ph�}k����
�e�i�m����m��d�� ���>������r���$R��uzP��k\���|��S�)��9��~���Cq�����}��d�%6����f���h�;Tw!��,��O�mh���'~�c�_��1E=K<�����a{o�4}_#��p���5��:T��Z����m�}�
��
lqg����=h�fi$/[����Y���TX�|�=��Tl�wja��^�����T���
/<[�YWSEG =�@o������rp�����u*6���qC`x/[Y�.{�.g�j���l,�I��R�8�_�h�)��v����`�v�dx��)�{}�����J��}=!I�+��a��h��l��xz��l�mp�����ZG�N%ld��t���DJ�7r4��J���N�`�qu��4�td�-�4u�*Ky,u��8���`��n�Q�V�/.����k���g�jx��Qu��S��6
v����j^ws]d���������q�	�W;4��|�����'��>
9nO�r|��I'�9-���V��^�\G����`Lrq��������u��~��M�0_<C����w��c��)$-�n���m��6�r�]�g~�����7uK^��������z4|r��"���[��
����h��f|�����l��\�C6�L�l���7W~��er����	\�U�or��G�[�xo�.IlZ;��r�w���&n�-��m%��7-���-�~7v*��8�����B��*Q�9�t���'�i�MUm(��^5��Gm��n[l�����n�nm�vH>q�A�����rA�K�[��H���.
����f���]��%HVW�����e����M�lm�v�]�|�|RO����	^�f�JuuuL.���<��XMB��/A��{�����m#�h9m���r�2�@E���M��
'TJ:�v^c�p�7��<t�5���_�H���
�	���k����6���m2�e��6�nO�.I���=K<3��Yd��1��[�fgwvR��f�R�9I�>�m�yK��z�>-�rO��H������G ��'�&�u�'o��i����}��Q�8:���w��Xk�kZ�!Z�l����[��q�N6�$�&�4�) *I���v�H��_yV��3�j��2�:.�:��������gj�DE�y����u1�[��_��j������G��%X�ww���e����Y�� ��Y�����(�[R�w0�f�����;w���3��_<�w4C�G�c�#'?^���1r�CY�}��y����������"�K;������N_O=;�{�u���o{�����=���8%�{�d�����i��������9�g,A���=�E����#�Fw���V.�]R�mzY�H�����3Z^?&��2�~�4�6�Jy�_,��x[����as;�#^�/[�b]�y<#}�m$�iW�]]��	�33���z��?xp�V����r���w��hs��}AV��eZ�8�B��R�"+�L�.�����IW
Y�w�5�cu�-��Sl6�3ju��T�s�W|R$#&mF��n����I�����f��n���u<��n�K�u�j�jH���M�f������i2�Cv�-�}��������QQ�wK�,Z����s����?��k.�������i�����	m��������m�76���6��wv�	���9����w�U���5p���6�UFC}f���pz�tM��-��m�����m�6�m�' �O"m~��[�(������1^�"�=^����}OU�)e��pPo�R�=������I��>M��I>I>
�I�/�H�+�O:����Vq��y�����Wce�0�s&�4>=����[m�m�v�;m��$�(�����}����j�� k��;�����;�
�d���:^����s]M��Y�M���|��c�5$�������9$�4�
�gv��E��cG��yjw[��c���<����������L��FW��Lr���92Hc���r}��O�NI��z�z�Ru���'( }
8b�t�S)��B3/K&��:7+~�����E$7$�� �' I>I��nO�����b���I�:43i	�����F)F�^������{�i�s'v��f�5���)'��!m����;��wm�l7v�n�>~�����3�R�%
����8�Lx��������7K����Uo���	n��9m�m�fm��m�m������dJw%��3GRf�����c���5�v����{����N�I��k�&��F�{�a�vD��.�v����C�t���P��F
���t��g����We.o�Y�X�T:������DT�������*Bt�����&j�I��5�4�{�Z�9=I�����{�r%D	���=S�k�^�U>�����u8�����c�������8����
[�n>��A�x=Y97N������v��d�����k��v����{���9�s�mO/�)�'+��^2��=�����ko��
����
IQ�o:��Vc��jr���5![���es.)��X�������j�F�^f=]mv`a7��g-�pR��:���/*��C��rY1v����~����Q���vK�~2��;�����=-� �}�����j�^K�Q�&8���!Y������+P������et
=��/���$g�
g�7zW�l�����%$�� �� ��'�I>�i�mk�q�y�w=T�b�P�����r��<l:�M��P��p��]>�� ���O�r|�$�H�4��� ��{�{v��o�H�*'#zY��V[�P-�����[��W�w�U�vzx|�}��[�9 I>2H�|��'���������1	b5g�YX����_[���O���U���'�7'���E$9>RHM��O�KB�p;{�#�����	�����yS�{u�����
;'R�M���$NI��I>��'��
�>-�������W���fri'H���i�Y��y�w��7�_���q������Yu��r9>�� �G'�H�p|��|�|�
�y�=��|��f��w��|C��vE}��]9*S�*�v=��c��X�''�����9NO�R@�|r|��Un/-�=8$��K��,����l^��N���m5WU�������cs����9������e���h\�G6�������V���\A�]Tk'sUD,=�M����o�"�jIO��S�����l�i�m�l���3m2�d�m���sKZ5�[CP��+��+( ��o.����!�Y�
��i(���]��6���9�����m�3m�m�Cww�*�
s�<�J`�q�s��e�T*�~X�#Wt�wL^����du�A�������C`��~���"�f	8�x{�	_���^������l�M�.z�swA�wZ*��t#j�2�D��*u��
j�^8�����B�gc��������9s����)r~�,]��%I���xz�%P����1��C�#:��4v����*\��{f-k�����gV�����xN���P��*�1��|>7������'*����Zy��������X+h�[�0�f���6��CC�:��!3�U3!u�P�uf�|�,���NU��l*���J^����7�����������2����a|��kj����eQ����A�u6��w����57��b"UjC��q������C�k���!����f�K^>���1\(S6��V��7K)
�B�5D��Y�����t�}yr�������R��/��q��ta�R�q������z�v��u�����rO�r���������|c���E$'z��n;����c5%1w@�:�'�������~�g@c�n���������� I��I�I>)��rA��O�����xZ�����I4PQ2UB��3u�s;�1%��z��������������� �"nO�r��)$rw��U$3}�i����6��}8[�6(p|S�e6�_Y��xm����=�r@���6���Csm
��9m
�lm����gC]�%���*����'i�@P���b�Yp���]k8f�w[Fx�p�Iim��`�����m��m��l�$����c�W���;.�^`�AY��-�.E\l	��PHf�Bm�W7w���������dm�[��m���
T����LN����yq0��F��+�b���=�1B��;��df��<��6�fm�v�Gv�2�F�����$E$�S]�����{��f����J�;iI�o�I.�{��H�J�}�N@�i.�B���i76�;����c�^zk����^-���L7Y| ����O�5���j��,��uy�8�������M��F���m��3v�f�>�t����.�����M'�.�{
|8.�1<^�[���V�aN}}��t���i�m-�\��2��o�� .I���z5Q���W�6��o�gQ�A��l�J/i��;)�<�\����Z�=3�{������K��B���\��w�7�����:�s�B � S�����' ���B��{�����DD�������B"{�����b�H�s��c��\��JHe�k�k�K"B��!
�>��7�I?"��a�aH���|����}��u5���v�AOf=�f�-=�����A���i��}�3�q��B��C-��O���5:A{=��f3��/j$D@)Mo�Y�snM}�k��?BR������� DP�}�u&�ER��~���R�'>�������}�b�HRs�;z����������$��$A���3�m6�E �����;���wS}Lo�5K��2���&�Y]����fAc���[P��c���K^A�$��f����������TK#y�KhD�^�T�h���N��Cy�j���[�*v��<�Q��
@�q�gzw�����B���y�XfeQDH�I���=��BG5�M��H�)"g<�q��sr(�+^�������_=5 J!���s;��'!
P��ZZ�?/"a����"�;��E����|��u�R>�mEB�
�wK�1�6��f���Lp�=	fgy��Z����1�	��}=�,�R�{#4,��o	�W�����}s/N~Mb��"�������NQDA
���75B���T�B!B������N����!^��j�V�R�/��0���jE7�{�~�.��	$�A$�^e�U���/G��WgU��3�h�=h���MZ��K�?:sS\���J�u�2����7�����*}
�H�r��v]7��;\r����[�sn��^�R����L�}���������I#;��V�H����������;3������I���>����E!1�;;�w�3>�&��AE1�z���=�5�)"DW�����F�)����u)%��������'_��:��!�Q*��z����t|r�m���&�rg���~����o��;�1�cn%��u��V��U�N%��;>������"#yk��gI�(DED{�G�|�M� ��V�}o����0"�����o��A
(�r^=�{�Q�D�>���{�nME������v�D��H#K|�6I�y��^���=�����%m�8�1�c#��/�8���t�w��7���F��y���>��O+�����l���f�'V�UQS��I�0��F���M���U3�%G(��D�'7�w�����)B��5��M�JC��m���EL������C��Q9�L�[�d"*��[_K�R�_>����")S1>�q/#g��l9��4��;�m>�u�o'sL�����aT^��:���[�����U:��+u;�=V>�x�LKT
�����P5�,�GO����Lh���%�?{6��(��TX�()���g���A#��Z��)E&�������j��G���o|���(!B9�����/�2*��������m���*�(��SR,QW�|�=�~����=z�#�&_��b�;
�M?<��gaf��V�1�s65{;�m]�T�sp �}�*O���Pg{�=��`�yP1,s�����$��Nw��M]�~���KX����_�SB�""���9�w�o�&=5�����c\�H�DQ>��X�����\�P�D�x�y�}��P�Bv��nw����R5y����2(�"�����V��:$BE�}{��K�o�&m��$
���������v�e�{|y"j_� wg��j��n���	��&��M9;���t���K�5?]�`�6����s���Ob��6s(jQH"���q~��������k������@N{����R!B1�_�V��AD��������s���BLb����q!!O���}���o{��R�A���9=sM,�0��V�^�������G6�&	��w{>��T����X~Ge�������m�pk�mqu�����Q�6�w�B���&E��Zw��zo�{��% BEu:�j�!	��=��nT*!"k���������+�N�k���)O�P<��B���y��9JBA���[�}��)HD��q��\Rw����("�l���K�q�>aV����xH���H����$�]�i��(n��-e���y������������C|���Pw�������R�S��mX�~�q�B}�}z������jT�W���0Q
B����r��"*$"�H��c>�sY*B���g[�7�������w>��5�}���� "$@W7���I�(B��w6��������t�_�i���n�xv0;�h=�}X�<���=��8���\'nm���~����8��c��7�?[�xF�r�&o�8�ok��f�{Y������H(��!����/��������A�w]��rr�!(�"�jw����J�"������
R(���w���[F�������z�d�����w�������D�"����=���K�=���e�v������J�P���c�z_/����%!J��|�2i�������N���DU�����+���7e�z�b��2������p�A������������������u��k>�3���H� ��70�;/=�t����(%��W�o���
�?���A�ok�wV�/X�s���g�����)$���`�6��`�^=��\�Y��
|%(HA��q�����������h<�9�NYD U"A �O[9��7��2/j��4�k-.{�r"�QH�����u�sI����N�-7R�r��tU2~�a���y���*�`y���5���[s���<�j��9�
#���{�tVa��)->����LO��{=��7����N��`���'��������" Fo7�u�����B�������v��BPT���/=��{�����D@"g����o[�Q
(�-��o8����R��}�o=��?g{�+"�ALk�.�O/������s�*�R$BADQ0�n�����8���gj�����qb����B���]�/r���sS��N2�^��N%	"BD �'q+5B%vn��%��B�s�B�B+���s{�u��u�M�w�^��^�&����Q! #|�;���_��u�����y�G�o�qR�DR$k7����0g��{|gX��x��B!EDV1�3�//���o��w�����9�w�s&�"$EX��:s/�����|����{��k���.��RB���W�Z�����8]��������s7��m���C����� ���+��}u���Q(o��9��|�%%�6�H���m��JS�C�<=���>�q�����N������������x;��gV���'9�=�[�B��W�5�j�5���P���'�v�9����1�"�w������$E
5��~����l�"�H�{��=V�����=��q���^��Q�_c����;I�E��r��5�B(�b�+���I��Z��H�#,g/���w���{�9����k������v��T���s���B�g�B����Z��W��� �)(�jw���f����S;�����=��
JDD
�*%��Og�{�zs;3���=�n��m )"�
a���G\f��	��a������h� BQP^%��@�[n�]���|�gr%���@�E!���o>n+#3{&8�n���D'L��d�$A"H��Ds}�n��o���gy������k,YH��7r|~�'��TY������W#]p�z���U�2��(H�z2�:����LZ�����2����������"��;s��e�����m����j���a8�6d�b��:��x����Y7��uDu������`!AS��w���{w]��Q������B@��g>�u���}�R(��R	_^��c]�{��	"�5��7��I���v}���@E%D�}����*l��F���/N�8(�$��B�u�g�����G�w>��U��R���H$����)I1���.��w[ns���3�=�g���B"���_p�^�Z���j���M<�w|�)�D!D��OCU�M�T�MWeN<�r����/�(H�"��y�u�z���|����^z��B�R$R�[
3j���=��S�R��:�s#�r)")I�L���ok�w5��3s���\7����f!>��k9e�������`�Wm�� ��D�!z��}=����Y�	�]�v�n�8�[2YK+�� �N�u�Ok8���{]��NG�4�Q������nx�
D]8Y;w�+�9S5�U�O�N�"3VSg�2P���K�����T�S���G	��D����u�����(E�D��cA�]-��f
�$Ok7�z�� $}��z���fi�H��_~�or�B����_~����� D9����}������w��j�R���s���	Q4�./U�i�U��)�����{o��[���ofA���d>$�I%HB$")�Oe=�����1�=��������{qR)DTD������n�������5Q��
54������������|�����_9����!��+���]�H;	�����b/-�T��$�B�	���}�e�������s���k�8���L���@!(U������+]�B9������HPPR���������;����;�k��/\���^2��9.EU?#�In�3��Kn�t�"��S���=��v�TQA]�����]��z�[C�M���\�_Y��v�����4'�Yc_.hMD#0�6��_���m.�.������o){���l���>1��,^�{�Ei��n?'J��FD"�����__q:R�{����9���
@)]�{��y�9Z�)�_�����B��[�~sSb�J�?c~������_�N]����HA��?{rj���@	!=����������������������T�DP�QQB���d����#�g*p�T���.��
(��Gk��{���~{��>�"p:���|^�>WN� (��
R�!M�������N�={os������>��`���Y����}���s<���>�y[@@�'NK��os�.�OPIs����49@P?I A�X��8��3���9nS��m��~��U��Dk�a��tV, 	i��y|�����=��y�{��o���IAE>?
�9��b�K������FwXw�c[��x�AI$B!S���2J0��9cXU�
��\^��3��T�W���o�����T���z�K�<���D=�Pa8	f����o�R�7�����z�����T�:��3�A��s��:�7*�H	���_���L��E%��������B�����9��I�s<����U�Q ��_��}7�������<��o�n*	
��sw���Y���������/2��{��R��"E(O��z}��37��s��7��]�|��9�W)D�E +P�}Y�P��gq���0��DE�B�"w����_=�e��owZ��������/���"�Je�N���g��g�zwM��(�R�HD��1Y^��;;s��{�s.cS���q=�����BE�	���+�|��w�-��wm�%`3-_&/��THQ�:�\�K�8um�R�����_P�$EMt�w���������
R���m�����e5�|A~ �$8I�T_nJ��q��8��(�M�%gx�n���rb���1�{wzN�O����NI����8�`�J<��	���4�<9��X����,��iX����UUW�_����I��{S�����������0�/��u�JR>}7��o�V"*F!�o��|��S��E'q�87��P	H��7���^��V��B�s�jwW�
Q���r����B	$�>$��n�<������
M�a�g��s������d�u9�����>�������w�}�g��)@
EA�Ga�8���p#5.���H4��DE)H��\�u5�����k���w���\���DH*	$������>��1k�s�a�y�&o��1�sz�q��Jb�����u�P�y�K;�Og�A���$?H�	!.�u�T�:��kw��l�������5IRBB"�=�;��t�3�
���h.��� �HE(�r����o3�N��o��s�����3a��������_n9��{Wh�y�.����z��h$�@$�E�[�\�5�[��oK����j�6N�1[(�=x��L,���N�e:����BC�/<����G�����5�voU�*XK�yR�z�-Z�
��I�#���D;���'�w��#��g��ux�5s����(>�}�~�PD`��� �[���M���\��+!��$@s�g	i3	�(�����B�PA�^~���c���%")"������I�x�����s��X�����T@A�2�f��am��3�$�I7c-����o��k"`�m�f��^r�L�}��+%���Ng/�W�v!�7(�=O�u&
���+������Z��w�4���u�`jmG#�
�b���B/]���Z�E�E�x���E��@��4������# u@�D\���V�P����K&R��e���O��#�0�k�������,k$-�{P\����w�OB��7jwKx#�k�w'%L�*�+�����T,��#'n=`���L�3"�M�kT��Rdd��������B�o���g+���:gS�N�9�[|c���K�t��
��Z�rVf���>[���7�*=3(��yf����4��Nvfb<q7JTr��Z��|/wq��7�����?��T�Lv�SU���T����+��,�����;I����(�jm��1��Mh��Wa�SD+)�
�l���D�����y�h`�	��Y���e���]���w=�G�nq��w�������]<���e�\z�6��au9�1�U��K�o�c.���X��*%.�r������}f]��a�YGS���_X����jc��5��\�?��� ����0�����V�&��.��b�U��4�P�����6����
��<]'w�P�n�B����t�^��%����I���
�.�K�����;�Qhu}�N����{���&��t��c3�����������VZ����O��	|��>���G
���?#��5����:�MS��z��Y3�k��7|&E�H]�
�^�Z��[j�Lu�t��u��8���c�u��s�;���lS�57���m
���]f���Y��q0�7���]��+A���2�����.��B���������d���`q�G5M+�����l�f��v9��V� ���6j��uz��H�����s�p�S�����\8s��:r�����I���w&.[ai�3%��d�v>��Q4:�2���}�D.��W�Pr�z't���Q�#o���5��-S�{1��pO����NC������bgk��\�3s�G�Rr��Y�g�|��f��U�n�����P�i����9�����oFe�
�tZ��n��u;�K�:���\���L��gt5�9$�������e��m��	pU�
gQ{��;�(��g��[���������e-��f"�5�3z��>���v�)Eg;�����x�4�:��(����/����U���n�dI{�*��
��n6�3�@]0N�{7�����}w+�����
��v7rVe�KeV�����p1D�����K&���rR�kh�q��C�����/
�5�w:�����������
��ES"�X�x=54��C��2�
�2��j�X�r!=���^�d��Jk�Cm�U�E�-�!���|��[�f�WC:�y6�Z�q�D.\xDN����`��v���=:�j�
�ye^b�I����q���WcX�sY�	=zv_wAV�����T{�]>���i�\wuMT�#l��yd��C���$���y���e���u]s:B�0SU+�I�v��Q���2�Xo������d��F���7d<���=��QR��#�qWu��6�4_P������Y�4�c��_����5q��>�7|zs��-M0�#SD�g�����R1��r��p���V
�5��z�S|��sWR��coE���6S0Dw�C��5�!��W�;S�������9E�	[�NX���,a�@������6�*|������:RkbW<!��c�F���1�vE�vb�'���v�t�v�v3���.f>W,�J����M��
FI�������8����:'�0FNk�h��|����gN=�5�#�����om��so�@7��r�*��fJ���z_U���m����&�C]�Vuv�U;,,��p6�.%��{|_^�.M�Nu�]����FGb��e������L&iE�X�\��z��A��~v�S1aPY�Tc\s&M
<���Q���S��[������o2��w;nsf�����N��"�~�&�����Wy�}�m\r�_|:x]��DYk�������'��������R~�E����dcKm�Af�V��r��S�����?RU��0��J
������w����N3m�17Wy+��M��{���GLAdOm���L�c���rC�����w��B6�L�+�xe�t�77�����c�kQE����~��wr�sg�keK`xX�%�C�SN��7�����
']�^:4kj��D
~��u����1�7u^Y[�zrM������{�i���ZriMd���#D�u�l���5h��,4j��Z�E��x���o�2��b�W���/S���3B�
��������3���q
G�g��kd�TR��JqA�
@��_d�������YiJu�j�{o�]x�����"b�������d9��h�����qm���E��v��='mhvI1=ut$8��6��Y��qg5�N�:�>�����Z)g��9{"�����9{�Q�U|^;�����c��v�v��m�n�!sm-�Cy��=��b����M���]����\�U�oe?�n���kW��$9�9r����E$�)$I �z�r���#�u���@���y�K[�����q�s��k�����9�s��w~��h6���-�6����v��lC��q)��+����H��;Q1,G���Za/�N���)�h{�|����7m&n�
�l���[C7mU]��9�,��0��S�N���3u��x����[��aP�xStf��Z���?<�}$�' �7 �''�9�@[����|\����_)�����]f���/��8�^����C���y{��r���������i.m���n�A�'�I>I����{���L�������c�)N��
�xg�����I�� �����_o�=�%$�H�i�>m�>RH�I��$�}��AT��m^�;G��N�20�7�E�*����r�f{q�%7�����YnA��@r|S��\���|S�����_�G�y�_�k�tx: Z���Jp�z�i���w�;���w^����F�m��O�.H�|r}�����'�g�}�h�Zx���,�};���������7c��n7��^A�~�x���$��'�)$�5$9'�9>�i�o���5�K1c���*�Ky[����,��:	������8�3��V��C�=�W�wGl�1b������U22�3+�Z���V{G
�b�V�w8�b��~��:�D����J������NPsF��P����#���������������o|���`��}���<F����g1�^��)={�*�L���.����,S�)���d�Y�/Y{���c�I�n��}�FJ��=�jC�Jj��c�z���[��DDe�lx�����N�WY}������
�������)�8�f�b���������[�q��[�gf�j�q�=���K*�)@�{5�x�2i���{��s�<i�E����m�]��aA���������R�N]��`������9�'��r���}ZI%ZV�4���u�b��[ (�9��m�S�t*����������s����m��yo�_
�����S|y�����%�^���[�n�6�r�v�f[C7m��a��<����3M��)�/�z��/}<��q�--;����D�0����y���v�\�an�%�l�C3m�����z�}�`y���F����y����G�R����r��~��r��"�����-�wm�-�9mr��m-���`�I =�n�����
����WV����w��U�������L]��&:�}n|ww��v�3v�9��[l�v�2��i����.�a��^W�s3m�����iQ'L����U(%*�t��-�������}r�N��9>
9R@\������'$I>��;���{��������pL��+G5v���z��u����h� �� I>��I>9�@r|��$�Y��]\wWH�nb	�r���G�+��\�v�li�����/���r�s{��\�����m���m�����g<����T[C3z���R�C�h�1Ex]v�� �	g�����<�6�'h�l���6�v�m��m2���c�<�vA�S�i�W��k�����-�q��h��J����L��]�%��K���m%�im�-����
r}��oE���{����uM#����.�
�����n���nG�b��<�]��gn�}���I'��'�9>	I>�� �) �' 9>�I��};M��`�RD�K�{1�c�*��5WN����W���E�{�������L``j}8���qX����%�����Mu�Y��9q=A���9}�l[�=k��0�����B���N
�nyV���(<��&c���m�Rf@X����<>��v^�K�=�vXhJ��n
�=������RS�N���<
�5��UX���[�'�{�9�d�lxG�D��8�6��������HL�^���V?{�+q����������{V�|�E�l�w���7����H�z2�������
���
����vB��[*Q�����:wn:��M��W�l�js��w�������E���Z�j���������0o��q��k)�C�Ey`v�x@����L�����hB�=���'C�3v���'Z��T[bH��)�6=����B�e��g9���_���2���B�l�*`������0go*�2��5���&�u�n0)-���/��'i�#/wCy=���' ���m7m���[mm���Gm��������;<���j�Q���Z�J��`�����tX���#J�����x������hfm��m6�d���1��A����.�M�yL�i<�35�����>����:��
�:�%�����I�	$�9>2H�@#�[�����������g��s����+����V��}�z.�-M��S���I��M���H>
9>
9>
9>	9>I>�9'��w������l��W>�a����JTb��y��{Er�vfv����������S�$�v���M�h6�v���I�m)��Bwe��m����Y�����z���Y�cj����a��,����)$�m��l�������A��L�l�m��s�D;�ITgX��+#��nW
�Y�I�&��&��2��:��>�� NZK��e����r�G6�����o�w%F������R�a]�y���q��~HJ�����q��X�����9'��ar�K�l3m��m��l7m����t��}�
2�����G��g9��[^����r���{^�����|���lwm&�i76�sm�3m��������|�|��Z\l��K�q���^{)
�~+�w����;���`%%.��]��3m2�Iwm%����h�������/UsM�Z�1����=��;���}i'~9��9{�xCn���Y�=�+�Fez�+SF��q�w����C�wye�f\d�@F8����������Z�P�F#s~�^v�G8dy�9EDw?{U�'�Kv���gvub������K�Qo�xx|����oo�4wUe�=c���d<���=�To8�@��N:P���������u(�*rs�`���}��&��<��K�����0<>��g�C��:�3N�8|.������ym!�
�k���y�'t}lM�'����w'�^��0���	*1�����q�����@]��,;���^r��~�c�]�`�v���j��:e{���1�{`�@�+;a���*��kJ.I����V��u����-�-=�V��y���k�����u��M�L�������qm��N��R'F��E�]�l�z��8�|6&���������T7�8��/y�v��C
Y,���
�i�mv�f[A�l���v��r��
��[
�����A�b�-�K�}��������2y�������f����m��3-��l3-�m�;$�' 
���\}[��
�w���UQ�f���������*M��Y����������
�iwm-�\��6�$�|c�|���wV7����m�~��u��=ajN#��]b�����E���#����z�r|S�� 
IRO�r��7$&��)w���U~e���~��Z 8�����:�B]cSy��
��r�+u��nA�N6��'�� �G'��>�9 �) ���Rj��z�x�[k���WT���Z����%R(cK�+Dpy��'�7 	��HS�����r�r|\����d�/w;��_v�;S��;t�3wm8|2�c�����r�;���_zg���'�H�2I��9>	9��Q�$r0�;���M��������1�K�S��u�OA*���]N=+r��xy�{��9$�'�� Q��nO��@#r���l{��>{��`������w�_]�������z5SC����2�����������36���-�;���l����3}�T�����/H�'.���%J%���������X�]uYA6��N@�����|S�����[�$9 9>y�O��1��I��7t�"��4�;}�\��kw)�$���
�t�������~��#���G�DT���#<8W@���z�^����:tER���s��A�Z��=���B��'>����/�SrT�\�K�qZ����S���6�<�}*i}��t���91�xe>�$K���q�������0����=�n���y�7���p4�����x�y:i��������q��5�����:���3P�������;�]�xMxi9������Uj��8^���K�C�%������v�O����b�	[���E��S��fx���tn-e�S<��W�6�53��5j|��rfjp����-O�����SU��<�:�Av���L��i8�R�z�"[��(h� �����:���{�{�J�d����Z�n����Yi����(p>������V><2S��*=�/�;U	���q�.�����t����KtD\/�2�u�r�Z�f6�y��7 ��"nA�R�7m��wm��l2����s�8>�h[Y��/�G�$����c�,y1��g��1��a��{=�}#�G$E$� f[`�����k��p��������W2�U4T�Q�<�+s<(��V�m@��9�\k�~�_X���m��K����2�F��������v�f[@��`��k�>��u��<��ia.����:��V�����ut��*
u��I$���
�f]��-�6�
�l���wm+u�����g�����}��e��I�LF�p�;H����TE.���w�6�m�-���l��K��wm��in�9�������<�>8��V�w����}���
e#��Jg�� "]��7�'}y�|��(�	�NO�r|��9 �>[�E��
��{�;�I}��C�'2U ����"i<�z1��z3�<���A����7m�������[���n����V�
��\�|	�HC1��>�����>@Xr�D�w�������n���l���m���Ir��l|5>���X�
dK���$x��o��T|=~���WE��G;k�������������h[m-�������9�������d���q�z&�p��8(g�X���X�\-
��c����d����3m#�l
�i�mwm
���l���/��9&zi[%��W�9���)~�Cra2���J��S7Aw�����/�}�Z7g�����;=�^��`��xa2��t@�9��Gd�(��9��k:���������m3�	3W7<|g�U y^�v�T��`�������~���p���(Lms��c_�y�86n��:=�p#�l��<��t������8��~�k�0m8������z#��#d��{��q2c�����7�C�x@U��h�9��^�7k��uD�r��5j�\&���9�|����/�rfI�pm�.2��)��UI�N���������F�Fc��TX
+�.e�"�N�fv�7	�m,�:����TC����z�S��G:X'���s3Z���r�}�/6�=0:/\3�zr���7\U]�rH�
�ELS(A�����}���A�ff�ofW<�S�m�	7;k�0��z���Q���F�����Ji���%'�9nI�n@r|���6�$I'�����[�W�l �����K=s��^s��t�q��|��U��nhw^��)��R&���	�|������	��]y��+���^)�w)�G�.;��'}��������S�\���M���>-�*I��I'��'�� .H>q�>RI�.H������3��8�t�j����C���h���Hk��f�����cu����*�{pv���v�nm���6�d���sn�77~K����r��'�3�.4�Do�������mH|V��L��x&c��:]�d6��������m#�m��r������F�w����_�d��cdL@�*�����V������>.J7m���fm�;���l���r�M�����S�[�sT
��(���_[���d\Hau���9=�y����n���9mv�3v�r�l���y������IoU��G=��I��Yi�wP�����3�k��gF�������l�[C-���e�����]�����������n������:QKm��/`����eF��:��Y�R����;{��#��nm�9m��h[m���� 
I9>��������5������T.�a���V�6L�-�R[���X�����9)�n@�������9 �RA�wkg��������:�e[C<>	U�7�'������M`��9��Pw�x��/�?x�*�J�Hc~�����]�o[u|>��\�K7$5��>�M�7�\���ceXf��j	����YQ����k����/!��1�t�ZJ�E?xW��ZM`�?f�P���x{�WO+��W��hC�Zc�����j,����nR���_g�kKMS�7Y�ES���vq����b��e:�;��D�>��o�%��b�t+��
!38�V;��k�kRQ{�pA�|Wb�%L'=y���������"M<��&���H�7�6=�g���e����mq�i������r����o&����U�f����2{:��Xq2��2]�w�����E���Au��k�:]]���b�mw�M9�S�g�J�i��1��s���F���U|q���A�����Rm��T������T�p�Hxn�u�M� �u����;���i�m�n�
�lm�6�6�r}��n�.D��o���]U���UJ��RI���]1�7�M.���fI�����v�v�&m�v���fm�NO�r
�>��G0xz����wx+V�M�xW+����T���rn
�vwj/<y��M���hn[e��36�m�m�9 9_t�yC{����\H��y�����&/���#����10��Y��F����r|��8���'�� jI��O�Q��I�q�Y���d{�3�y.a�E�T�r����`k#�8Zt������;m��i3v�v���7-��'����3m��EI'�������������c��wD��\vU��aE�i2��-�.�Hf[c��7m�n�N@
�w��~�^�Ux�d���*D��9�O^Z�:i�+�����f�7n����\��'��r|�|��$��'�) 7'���k��^�ooQ��d��_Z
j��ro�1;���V��K�Y/���''��'��>I>�7'�9*I��I��r��g�OQM���z'nF���*��e};nRf�Z���v��E��y��6�rHrO��}�I$7'��'�97�/{}n��,>��8'1k�u��+���
�s���j/;�O7���H�@"���E$���7$������
��y++��C_�.D���9=�8sC^SS	T&���G6
��0��.6T����*��6<���g������������*cn�uq��p�����I�go
u��@�
�y���V2V"ez�!�&����r����T0I�������.����vb��{����W�w�x�t\��l���^0w7yon�F�1T�����U]e��p����{������<<<��*��u��xN�Fy����������}�����
�TG�C&���j�v,U"Rs���Q�y������C������m������a���e��Y�m�n
-�[����k�i���j�{��������qA����^�q��+��2�����ad+���w����\+��"av�����6y�!Ul�X0�Y�x��A�g���J~^����y"��I�B,�}f��;+k��=ln��gU���I���p4��L������o1ws9N8�R4Y��c�7/����r]w��]�#.�Oh	9>q�R@"rr�-���l3-�r��y�����I�;���9��<���hv��Q���24�!��pKj�i�]��7'�I 9)m�]�F�L�l��e��?{9�.����tGs
�����f��c�$�g|��+��c��GV%�����rI���c��m���I��M�i�{�3��Awl����zi�Mr{2��~�S6h��oUWd�����/f��
�����h[m�3m&���������
�0�8 ��P�;����\������B��D+X�'��&�,����f[wv�n[Iwm6�w<H^���U�s< �*����t�W�����)��������jI��A�rO�rE$�)$9>-��RH���w���������oq��^������6�T���A|w']^�n@�����	$��!�O���������s����5\a��a�����OP
�
�� �d�=�����O�f|��4�7im����;�������2�>�������pP�SO����[�*�3B.n�Vr�������CKE�{z��lr���.�f��K��m�.�G�Z;�H-5�
�+^v��U��]G~�s����6�x�Gj2�+��o�B�m�����l���m�Cr�L�h\�~I��nO�M�����?`�
�(0|�g�����E|���\��/+&���@�A��E�b���!�g��v��c���F��h��q4�����c��N�(���
���2��q�������H�������P�:��\����_��&�,KJHv�������;��Y=�yWPu�]�������pg��yQ=�.���!P5��~�1.%���d���@xz���X��C������������E�	PmTbL���������n_H��'�]���1��A��M}�������B�]>�@9���mFt�w<��D�
 ��8N������w���DNA��n�Gq�1�j@Y���z�.�sZO�;��l�����q
������p�>�{�2[3"�5G�+�v�?k���n�~:|L���]/^IH�wJ���0�8v�$��&�����/{�<�*�y�u�&:v_`��9PV��Nn|^�X��9s����v�_f^Q��V�A�����p���hz����H�),����7mv�;m��[m���m����������t[Q�U���h+)*�'5�yj.6� �m�B�y�5��az�3�}���'�G'�I�9nO�R@����'$�u{k"�����w����:�F��v�	C/�4���e�
�J61����wm�����-�c���m ������'���3�Z'-�o�r/�7#�Q9^7K�������D������y�o�{fn�Kv�n�������D�%$�I$���| W�k�[�N����w����M���;WR��V�H�X[�����I>�9!�O�r���8�
�>9o��zW���.S�����%�!da�w��0N�(�h�]v��|u�G���7�H�)$*H>������|���"�&�\��R�C6Ve/h�7�a���p��9��Jd��M�mL���|\�|[��r|����.�L��sm!v��>��?V���^k�l�=��OG��a�h����+X��Ud�vC��_������9 I>����O�r��}s�*���}dw/w�&\9<��'5��������c�S���W���)R���9 ;Km���3-��m#v���e��N��%�z������'������/����;�����v���L���r|$�|�l2�����i.[M�h9m�������Y�5���^�n�Z�}�{{������qx	^���\�=�w��r���h�����}������=h���J6�L�3�s��)�#

\@���z�i��9=�1�UC������\��|'>��o��*m��K���w�9�`�7�������xf�7j���
����M���}����c�,^V/xx_\��E��
f�����]�';���{�s}y��JoL��o���`���S��k�r����!����tZ�ym����x�H��-t��� ^��7L����xyimm��U-�h�tI<73)��7a�m,��n��_vc���L��pd��[(k|��uG&4�@B��W%�.��EM[�����C1Q�P�R��,�h[�,G���feN0�I{Wz���m���|��y�V���UfT����i����~�������iY�������X�������|%��S�m��5a�z�A��O�JH��v�v���v�.�Cm�<���������^�87L�"�����^z��
���h�)��~���Hr|����9#R@S����9_Z���z2j�u�J�Y7����{P��B���^�+�l3�13�W�g�IrA�����`��K�l��H���l��X�9t`s�8{����4(9��~r]���
�����.��A�N@��hv��i�a�����sm&y���$Hy��G���9xW>tT���:��X�uC7U�
�|��u�5$�i����v�����m#�i��
�3u��^��3Iu�M���C���~J�B��\���Ui��w�9���]�;��m���;��m�wm&+)Dz�1B��V�w��
��wH����L���6V.��g�I>�swm��l��l���2���6�`2=����>[��>�"���7�8��G���W��K�<O������'{D���oC9v���I>���|d�@��-F�$E4���oY��k���hj
�h����'�{��'Jge�$v���&ZS��t��s���}������������_N���y��Fb�Ue*�S&pF�	�m�����&� �'�m&j�'6�v�I���$�}�}�����6*/'��2��4�V<�����}K��2�`�Oq�E����<�LX�
�$Ay�,{&�]o?V^���e ��gp�7�b�2 ��r5/��9.��������g>�aD�,Em;��W����(����c����w��Q
����U��D��^>�~���1 !G.����qW�=	�q�,��Z�������,��nAb�2��������������>Oi�>�������������^#���o�����Y��$/W�u��$��wc��nU	�k��"�DN�_�p g~�������QD)���s����{��R����}�� Res��4w[���R�DB�A@>��>�h�|�T��P�[�m��Y��Sd7S�&���x�/,��0q�L`�k;���H��]Y�zW��]eT������X�w����y���x��t�Qw�<�u���o}���:(��7�}y��%�P�������{�������'g{�B*"���a���99��e%n����w�Lq�:wX"����4��N�p�*�'��yr�������{T�v��_r_j�}Q��O��{w���bl���6%�-,�r'���\�""=x����DH�������c��"A�B"�*z�
D$Pc���k�{���P$D��os��I7Bs��{��H$�~�m�`����/�_�t�3f��������}Mc�!mL��X���[���Spo����{�m���Dl�kn�<��q�����d�;���,�/|��}7�e�##�������	��-l�&���O�$�d�uT(V{����{����E3����/��"BBM,��{��{"�Q[�y�5��"(�!���6n���d	b��^����������>�A���c����G�����<���1w�On���^.�t���'_���6Tn���'�V�.J��u�9�\���;n�a����?�kc(U�������/�P��w���!K���zn��R��,O������� ��7��H�}����K����E������7E(�����Y���ksqGs���������D)���Ux��M�f_l�6^���������}��m��L!X4G�"���`���6�y�q��V������G��mp��-�Xu�3Y9[�P�[82�8�sZl��Z}/<��W�[z����s�����!���c��?}��Q(�T��w�,AB!	3�w�����QQP#�9���S��������+�8����E")S\}�9�s3"�_w�	c�����H���!�9�L�K2��}����]w��9����Z��!j+�����O�����s�I�z<v��\[=A��Y�'��5����G���T� !;WS��(Q3��J�u��[
S�Y�E���G��H?H$
�~�7��������k������	�������������(P ��u5�}�;�l����>����H(�f��������w7T�o���sP�"�"D�����e/���"��f���zi'�2'}��t�S��^�'+^��\T������n��^�,93�cb�{:X�}�~���������J��I�7cbA0}�
��V4���k��$�;+��\	I�y��|pR
DA4�k������B���y��X��uj��~;�m��f�� �_/���T�)1�9����'w�$#��6�K����[�F�;~u�+�Z�Qy������w��6����V;�o0���/6Kr�Oe��]��FFB�B�W�_�����;[��q/m{/��^[��XO��M�2��j���V����o��h@ �I���_B��j�$RQ�N{��=�o�za�\
�"z��\�s��(Eb6���6D
D�����k�k<�D)N��������^��|�B/Li0�k����K��Huk&{n�D�����?�����k����V3���.�0��]`TeQ^�W�}�(.�ZY.���������]���z�}�������u����}�����9��������H�,aON��������DH�P��zbk�mJ�*�?7���L#�a��S���3"BO�s���o�z{��H�N�lff�����9�Mp�^��xI������W��n*T�(w�lS��%�c�r��21@Fw��Y3?��Z�/tZ��~Nd�E��m���?R�	�O<�������V&mw��uiv{�������&�2����!E*�~������B��"���MhH�PQ�����?��������VE"Bko��c�DTH)�����DDEb"�O��N��k�7|c���s��:��\����zJn�������d�u���g���)�P�-S��M�Ui{���F�=��738�����Q@�zY�q��'��>r���d�A����c>�S<�� �#7��[k;�[����B�����|����I$�G�'�y�$V+�;������6�`�^yw��W����D���z�}����"@����4�W�S]�s�z��b^�F�;��f_���[��s�{�A�	$� ���f7�1��[Y��5g5��q�U* ���(�H���56��p��]>�]��W�d")�B EO{Y�O.�T���n������[�}����JR DH	)���4�;{�9������g�������H{n�4j��!Bv��[8�e������+AE_osw��^���7�_5~����7�oxzVa@��@
a�k����K������7���
DH!{;�K�o�:P�&���,*5�sxZ����"�B��oz���S�]o��:^�wh�-��Q���\���WR�o{#�����B^�����,A���9��������[s���L}����9U_P���S�9�uV'��y��Z�{��)6��n�5[��c8��ndH�P#�v\��Re"(DS�g7����J(�QQ�f|�&��|�x���!�����E)�/��K"
)6�5>�a��71cD�>�yNi#��0��\J�Q�
�=+(�h���R"�H�v\�y�j9���!��1�8#A���\�'s?@$�O��J������������V����1�c��c�����J����+�����X�:j�lP��&
A@R�H(T����Oo~����c|�S�SOu�j�PH�� �D�<2o���u�7u�5�_5�w}����qAH�RBb�xyk��]g���;���
�0�GL|H"* �����Nvs�\�������K�3��z����!HH#�@ ����;Vn�����ev��y�bo:������RQ
@J�u���:���u����� �����9���RU2�H$t��g �DZ	������u�x��_h�E���pLy{^��)��P�=���]��d��~�lq?Q� � �S�����J��{��q�c��DR���rm�rEB��4�k��H�Es2�����L�QD
Az>��\��~�d�ADk��7�78�$U=���f���.�AA�Dc��	$�p�'��)w]������wY0EF#Qf�������o6�_9|�93;��.�HR$* $J�C�u�h�-�w)�o:��&�����*(B���;w��i����#���|�����`�@�!�@$��U�zu|�����C7�������DP�*$��@�������(��%82�i�	���x�!'=z������������w��9�����I�JD���|(|em��sO�'��BC{gsy�IH�R 78��m�Z����c&+96�}C��P��/~���o93z��:K��F�u��w�\���E��+C�'���Y���b��'�z��$�fp�*e��4 L�;N������UD�:	�WN$90{���A�i�`�JS���{���k�DH������z�`!$(����s3��h���;�j������	A+��0�������WT �$9���s_}�p�P`����������TA�Gp\9��}���1A�~����sY���z�������20P�
�HI���[9���~��3�k���1l��3=�u�L� 	&���%�2�V���Z���i�(
�"P)w���oy���ys�o���z�S���\����=�L(�!�C��_kj��&���0�w,R�%w|1PV1`��	7��_.B����r����wV�f�rX������	� A!����_W�c���[�ra�s����T�DT* 6��k��m%��^�!�����A0D�@D9��g��o������y����/�y�����Q��V��~�����-eg��:lD����/�"^�g;X��z������9���*Ok� �%/J\3����4�>X2j��]*���,Z��#��bD��eN�2��~?+���$�k���]���s�����g:�@QS����5�kn��\�%�vw8
! ��{�~w=�yB|0��b����a�����7��B(�������m�a���C���� �@+��
m��v��-xL�v���|d"*)3}��|�y"��Z&��9��nT��
��H'��o���z V����sT��uEBDA#}���k�w��K^��8���w{������DDB~ ��H�l�\9����%��e����+!QQ�����������p�\�e^��H �$�~k�X��������uI���w�����c�����f��i�����kN�w������E	�)"*��m�6��g\�s��8��w������
�B���)@'5��M���4�U����������^m/C��[=^!�:����Y�_x�������%��)g��i=��ki*�~t���Mz*"� ~����V \����X�}*{�G�H� ������k�$]���/:���������+��Jw�j*�)1���]f����:� �"+xg�c{��gZ�qE)�=�3�rr ���L�1�LM:����DD���\�=��{x��g����{�Ng����M��=�PH�H ��,�����K��+3E%}���R)Q�B�E'��c���lZ�m����2>�*�|>G�I �/u�~���^w\��rZ�f�]���H�"�DG;�����g��=����s&[(�`]�
���DDH�F.���w�����
{��_��=�1�nsY�������R@���A\��;j�g=�� o�������EE"<�b�p<7x�J�v+xnS.�����
"P�����c��r���dj��_�5��z�sX���_dRR�H�"
�*�b�D�Yp��b�w���S��}�"���������q�v��x�LF�>)���y:%u��Qyx���c6a�vV�^��S�F�G�@�H�R��s\��PB�{��������E!�c��D��������� W���;���T)H��������:��U?s����b�[79��m�h�H
Mo���V	`��:������R�<���)�B��'}���n�z����{�=|�.��z��7����� ( �E�s�-����V��8��R�����(���DT
$3=�N�y�O��)M� �K�����P|+�_B�k:`��7Qu|Pz;o<��}�4�w�����)'8�R���r��������>g��(�D"�������30����U�#ri��m�K�A���A�A" �+�����g��r�5����^���y���|��H�1b�Q���Ni��C|q�v��5������*%��'Z�{������{=�sz`��]�������g�nJ<�������s���;��{}wS��A�A8�^��JU�@�4��db��k
<�����{�����[� !$=�w=��9(D���>}�5���(��~��u���{��u����H;�O�7�����(�)U�~���~����]D �{��W��Y�@��B[��sk���(%��)��������Ws�������~���Q"���`���*2���vE\WdV�$��$|>C�>�B�-n��K���Kz9���s��H�@�!w�bk2��A���v��3/��(������$$�Q�gls��7��9�gw�k�^�����w�Ws/��$A$!'9�=�a9���o���;��]�3���7;��'(H*D�
���:�������g�����I��E"��:����0�7���.f�])I�K�vv���)
KFb�J3�!@�!���S��Di����4mxH1E�z+!�%i���}}�];�UtZj���}ln�nA�K9
����s���j�p]�s��J�JtVMt��OBZ_5���/}}���>�R"$AQ;��.�{�O�����RR�P�"DO{7�|�*�B)D�I ���sI�1BEc�o���2��tJ$(W��so��z�HBA	_��_k��M �"
-���s\�"D�AQ����/Z����*��*|A���_5���+z�5��hN��7w�g$���� M�Y��#�1������'��Xj���e�)��N�<5/�F+�� 4H8;m������2zh���fo2��K�~GV���^��lS^$Q`v�N�����g8�h}++�r��V�${u��U��M��5X�l;m������r����Y����Sh�8�U5u����h93�j�}�5����^��;��7�`Bs<��yBfi�T���]���
Y���'��fn�*ki:��G;��3i�8��V�>��U�������*T=�*�x:�^�0���l�����w(�2�u�)���]v:,[-f��wx�$:{������pf�Wn�7Mfp���mpF]�u&���r�0[Qs�nCW�M���u1�8�l��Z���]�t����j�X�ae+o��\��W��+���J#z��y��E�L ��O("�_dx��Z�ea�$*d����r��Z��U�����<���'ju�/9�sq�����:X��+[j���*Xb���:�E�����
jk9|{Uk�s�:X ��!�p�y��y�#u�v�nN�[����8��q��B��@��������0cF��o��I�;����b}�NA^�9�(�������*��T���/g��c��;e�}�f���n����+��:���P�f
T+�m�WZ����U�#H��c���kz-���`�
����6�f����xlN�]	�Jo�I�S�R�/	��R�}K+��F�jP8�Z�=���1�1k�������*v�$:��������_^��t_%i��{���u���������.g|�`�����J@��������&��5��3f�a��h��"�c���[��^�qvWes��x��f��w�4����d��f�m��Vopz�Y������B��m�B��76�yc8t�dV�Y;��<���VQ;��r�p�}!�;��7O8j�
��x:�xF���0�����n9��������v�7z�lAa�����"����������/+��n_bC~�J��f�Ow�Q��<5��k�_mJ\O�P��v�s��pg-�T��q�q'�5��Y�v�&��`�������w�	\�3yvCn�,����`�-��t�d����r�T�����!�"��xD^�r���)K�%����6J�V6����k���L����6M[:��Ez��k���P"���y��Sv�wlY��k�U�k�7��-%<�5�e%�1"�}O�W*/�r4�z�^�m���7b
�5�'P{�����tv����`�f�J�����ob�eS���Vj����K������QJK,U�wMI��lS��9������J(�u����*�������&�X��K��������K)��L@�Cv�e����������2�W5O�o�]�s���������e!�{C���s�wr�����b�������E�"�IA���*�^�L��rk���U����%L
���L�ev*��
��:��Y����L��F�,i������,�{��jo%�F*]*n\��.n�)V�w������QL���b��|-U�l�E������iI��v������C7c�.@-QG;J�'�=�y��k8E�
�C��-�"���s�A�����Q=}��a���\�Rj������{v]�L-J�AD�6ol:BX�WF��a����P
���3�%�xRT���4���{70
�f��zr���#Er���:U�6���vT�T�wx�+�e!Y���K�
i�~���u���9��
��,`� f�7$����l�u��)bA�-��P�>DWA\�%���	+���}���m3L�f�����es �ol��6C����z$��y��n�B3����K����o<�HZ����.�*AXR���$4�k��T�^jA
,���	
���������8M��7�--������S7��X��Qt��j#�V�� ��wEgY�E����!����^�3d�+�Wr���wE�w0��l������O����������U�m��(�'J�YZ����D����d[��	�6(S��=�/x�sRz�
w]��%�/v�����3�����tk
��J�
b&�)eY�3��>�1gU�����d�~��}R�i�h�,����?D��^t�����x=�����E�"�B�%w��m��q�<<gn���F���9uq�}�\�c?QG����;{��hD�3_�\�
�d�:pHP���N�z��2aEx`5W1�c!�.�f�W�����v����B�����������c�J&���������qeN.+��+�����J#l��}���r���*��;UEV-{�3�!��@���f�d���/*���s�-���+jc��F�������:��v��d�����lh �vw�w��S@�[
��7���,���^��[r,���N�E�jR�7�����O��Ac�*�sQ`_���v�g���Lt$��3{������&���~�x�+s~��[1�SVN��}]l�:��6R�>�Ns.�k��0}��g{��D�[L�K�q���f�6I�R{��Vd]������w�2+��J��2���F����������tghd�i!�&��G�h�U@�VHps����w��q�y��x��[�<o
����}��s�����'��[���3�������cBF���I��I�����������U�W!*7-,Yl�No7��)��;1�*�M���hl���o=d�4sv��!FK�I;�2qh�Y�'�f5�Y��=�'��y�Na]]�%��������I-��������>������r�LU'3��G����U�'��$�f��S�J��N����pf�]Gu����{����a?���3n�9�2���3:� ��7i�_g��������?O�:R.��b"���!������^M�����g��^�}��2NZ3��'m��8	���v�39By����=	v����T�����R��]A���v�Y�P],A��i��]{�NU|j	*�N]FI��H�I9�O��H��������R�9��b�{f�y?'�wr<4T0�/V@�����PI��e��r�����I;���������#�1\��F�9�so��F���Sc����h��<�n�
������6��F�&�@T����2���@�(\;'"2K�6��`X�{U�
�E�C�{�R����So.�Y����fW���Z.b�/3<��F�+u]�/."�
)��#�]P�iF��{��/sO_
t.:;���O��m���|�a�4tN�O�6:]�9eF/{�vUTk�x��lxk���F_��{�-q���Kg��z65�t���S��:�R��������D�
�,p3���]���]�����zj��H�C![����w��?����T�b����PjG�{�G����Wb�]����]��<z�`5lv�`���B����s���Z��,I+��J�(@��L�Gu=�^�:�W$�{����=�RB6�{4��
���]}5������\������m�L��lHD��h���2����Y�VU��p�T�We�]f�~o�m�%�fj�Z�M��m�d��Nj�'V����?��O+y{����P����YC�%(��X��Y����:��~�~�	'���i26�K���d�i��N����~w�1J�`�x9Y�(������k%�ks$��to�n)�lz��"���n���';i�T����m������C�����S�@�^�=E��F�����_ve���'��`�K���"����ERN����ui{i��S��$��'�4I��|Ch���mU�x�w_���������Z��]�����;�����f]�I#�^�wm7�d[I���[����������o�)���H�6]���hUG�7������o��}���wm3�I/C2�i��%�3�M�L���,�W,o���3�W{���,K�iu��H/p����u�PH��^)���{��i��I���6���$Z�UU����'�g����[S�<�:�O���;�+�=
�����C��]��}��.�g�.�eZd��$��$��$�[�I���I���<�F�f���$����8�5��/M���w�e���D�����a��}�I5[�I�wI"��Rr���M�1C+�H9�Zke2S��^�C�M��'����������tKX��l��IT>{i'[L�tLwD�T�n�6� ]O��X��NgZ�����MW��2T3R�����o9��(U%+��q6w^m7Uu��1�Wb�������-83/���9�8Y�&��������jX����F�����Wne�A�~5]�ZJ��7��&dU���]�F1�x^�}Nn�.%�������-�VJAy������[��� �cx^<4�YQ���yTT�V����}s2��<���(���c������T�=�����������:o�y�~�<�5q��
��+���o�Q����n^�x2��+�#,�E� ��==�����,f���JM�!���5�,<}�iM�F^T�Wv�=t���N�I�Y|��{��4��U����F�R�����Uwz�8�2Ed����H@����nQ��9��{���%���/����t�/�R������N�����EEjg:�����],,���]oG��Y��p�;������iL��z#�=��������%��'6�v��h�^���I;�dmD��y������ESLVtZ
�Z;F8���D�J��m�
cl��^���IT����U*��fom$P���%��&�s�����y3^�{;�����L���I�U��8X�b`�����^=���e�f�7�'439�3i��G����~9�
~�.f�I���=5���Gg�yRT�_��H����3�w�����w�2qD����Rg3�FmS3�D������������=�����(}9�c��C�Bol���u��O����z�)��v�7�'�v��)9�d��������.�g��%�ir1�i�q+]��/!)	��-��\��$�&��3VO�*tfoC$���dti��{�������3<�3kr,�#��=��[������d�r�xNa������wI�9�I���d��%��3�D��t���H�y�0�:�+����EG���2�n�{����3�K���=6Iv�D��Ij�ui#�'�wI���M��?
sT����M�q��[���.�_R������8�>��,��G�D#K����I��$��f�����J���Qv�hf}�{��w�s���vu�2�"��\\����h���s{��r��}��/T��$�Rcv�;i6�d��9��y����^�5e9�%��sn�;>�+�ZH����G54&���8)�j&Z�^j��]B�u����'r�/��z�Y-�;�AxF��-��4����)�q���N\���������$�ONR��_N�����9���!�v����%m$��x
�O��2 �[(xxr�l��H��{���-{�s�J�����2qx5��+���������/���p�{%�cu.[�M?n�mH���Nh������c�}UU�g�(�c"Ew���^��|%6������
l�����:��GCDV���H9���p#J���E�{�� 6&�+A����\��5�|�WJ>��@�H���`�+�7C���O+����le�����Xp1G^.s�:��n,����������O%��m6jF+Q�6mJj�-��g��w�����x(b��4�.��HOI.{���;J�}1����oq��^&��i�����4��{�\������I?��i'n���N�2n�N�i#����7*�����OV���R;�����/-kw�"����c����9�$��%�I;�4���O���}vO�j*#U���j,�;%jA�r1�7�[���lwG=d�c������<����6��L�3����n�2Z��td������}�w~�S������!+)�����>T�cZjK�FS��3VY��X�J��NU��JK����v��L��$��wW�_���D����K�ST�'���=��V�m�]<ZXM�Nh�������R]�I��5�3��M�r�$	��@Q)<���f�0yB�j�vr�w:o4�^��Xb�W�������������':�����3Z1m2&� 	�$��n���$��J����!�S���1e)R�^�2���[z]������w��U��e:�t���It���e��mL����g{�����-��zK*kj��vFr<�+��j��+r
�[�)y������������I:Me�fN6�m�h�f��;�I��=K|�t�)6�������U�|6�&��9;�����l�i����i4����
H�����FP|���u*�r\4��a�j�z�l�>��n���z��*�w��ub�����6#�w�����<��$wG-;�3��U���;���v���������>��R�g�eC�����r����T�W����bq���5��'M[4�Dh�����,�&�x}y����<)Y.:�^s����2D�cE�y����zc*\
1R�������o=`K��5.j��Fv�u��f�!1�o��"{������;q�;Jf�a�*�j
�p��b5n1����|H�����xxL����!u�C�d�h��h�R_0<="�m�����|<<*c!}���/k$=�}-j�]/Pj�2Tr�S�K"�.P�S:Q�c����Ce&9��i��SUz�����%��KN`��.�R\�w<������S��S,�)�w��u�>�H�����J����=�I�J	Y(]Bu�U�'=����)����;F��a\�G�r�k���(�����D�[:Y�j�:.�}J���K?�xo��7/�}��.P�'F�����[����o�������=��:��mL[I��&�k��g��E��|+�5��/kTP�-��R�T���l�V�aV���'u�~������2r�2d��sv�p�MN�������/���~�z���|/mI������h4"�����������gwi�h��D�$n���L�������&�����y������������j���Z�%`7o��3_��hT��������z����������gt2�&�$�v�9��[�/=�r?i�s�V�\Y��<���{�oS�4V_4� �����@Sc;h�Zd���h���m$�%RnUfN�p>U�H����	!�z�Z��uS��4��4��w_L<���?ghI��d�s���� ��H�9������6{�>���o���s2w@`pw`��e���+�Y������������{��'7i���-&iL�Q�wD����{�}����H�A��<z��Z�v��=�[;��8�x�r��NU[6HvI&���Fv�H�$�D���Z&~���v�����������V��
�c�6p;rr ����q���n�*��Y�}n�F�L�h��I;��mfwm�]���Ys&�uF(���J!J]���c�s*���U1m������l��������
;�g;�6����_!�u����Z6��B�Ei��L���^�Y!���Q��O��2�<�)�q_G!��!��0�
���C�}���s���L�^�m�fc�k�2�+m��yn��{�������0%���M��q$��mPb�(k�<
�->;)����<��'h��#\��V���.y�xo��9���;��'��"t+�rS���<ld�.����A{���S0.�#Y]yb����A{v_T���r�+w{����WY��c�V*xc����3���wZ�^�l�5Sh8
��2�R�w��,duNf������2+7
��"����rv��Q��c�>��
����v���7�����v�����R_��%]��bOfDq_N.����gBA��e:��w����%=Y/b��@���u���^\��d��tu���]���rZ�(����[��Sm�f)2����N'��7�^Z,,������	�{����}06\i���'��i&���v��BN�i'I/t�.���F��R5�;�#�;K7�%ct�<[�To�jd]�F�T^��MM��&�L����H�&:I>�vH4F���V����h����:���Ez���[���;z��?�>�a���
���3�fV��$���43I?y�1K��W�,�t#y�\�xk������a���.z��������5�-!��I���H7vL�%��H���V�v�y��������m7�$L��	��B�.�J:��������xCn��8�����$����"�&w�&�z�$�:O���D��g/�lI����,}6�.�!��v��d��9x	��s��V����v�K�M)�C$�S"�2�8���{<����{���n���g<f������������"������)�t#�I��;T�RI"�Y"&�>��&�P�U��(x������.��b��O�T�WEj0.��$e�=�:����*�*�W�z�uN�D�T��2-=���jv�O'�AA�|b7h�u���55�r�u/;��p��Q3n���zgx��Gj�q�N6�8�Gm$�@��$�����V���V���������=�(�t�k��n�����~��r^�2m	-S9���Q9�e��5�������|O��������wlt�tf~��3�j�\-^rW�=BL}�����t6����o��V�gG�MoD��Tt4q�%�q����Ly�jb�{x{�����VDt�`Sp�`3�t��F���5F��[�+2b�[�����tr0����GOo;��:&WgR��3�t��������q�E�o��xKfd�W�n�b���7]����<r9xx����:������z�~���QWY���q��KU����@{�F��mr�Du���:}�k���#�m{w|TkYn���]g���g'U=�5I�c�/�-u=���\(�c��������[U��p��I�~X�<�y��.U��s���I������/����0�pY:��E���^�F���Q�]W]��z(z�Q�V��d�U:as^\�g�6����i�v��V�}�����W�Vvom�z���$�@��M73mGX>��57�(�����������
�s��Mi���g$�H������R��R����m&s�3��^�����U�,pq)�}t�c�r������@J���*{���[YO%�]������&N�$��������j���'-�|
�'�������Y��Wn;F���q���gOPJ;�F�,l��f��}<~��������-�N�i���'{FNP��Im;��_�N<����}�+,^�>��_��oh�iZ������������.C=���U*�	��)I;m24"����td��L���Ww��=��|���r{��[���vV�fy�����9��i�j�����V���N��MZI��H5d����C�l�;��T����j��.�63v��!R��j�������2.�'{Ff�3�%�d�&�>Ud��==������2��f~�D�q�9Kb+�q�S%h�����������i2��ZL]K��i���$�r�y��w��o�^o����������?e�\����5�'o�,S��DJ�����.��RuTnU	�i2��h'z��������ur��o�M�^r]��ML�
����jF��"]cf�b�t=i����~����d����Rki%�NUD�	&��d�$�=�|��
���g
dZ�H��]�}��K(�tp0��[��wn�;�_�w{���&t�e��g.���Q9�Ci'.�=�������b��Y��eZ����QV���=F��]�=,�w����d����*Hn_T�����v7^���.���-����p��K��6�r���<�6.[`V]����u�1D�5w���AW�w*
�����R����$����o.w�%:���+��D��[�C���:�k�Q7737j����nm�=Ky�����.:����mu��+�����6a�*��}U��"Bk���Zx��}j&Mt���xz�E����fZ~��7�8��8���
�T�5l�9��C���T��E�a�7k6���*Z;�:���lV�V��r2���<�����6M���Fq���-����S��P��q2����>��5�x��L0n5}�xB�*e���9
�t���P��c�����K���`���qG}q�t�$�TY������^R��P20`=��X��+� �8�����*��Yk4h[������(h�t�W5w�Fn3����5���O��`���w��;����6���I��-�vH6I!��kr��cktp�3t� r�\����Sa��+<^�����{�O�>���������i��N�0S8�����L�����y��N���{{b�j-�,.6��q����c>F{7Y�y�-}��J�1�^}�����#uTr�F�'{�H�&sC&���dL�����1*!��I�B�����u �����i�FU.�z����Y�L��N�D����
$���vt���&��2���3-noD�q\����6Y?�h&��}�	����g����|�'-��h�0�i3m�q����Mm3~���o|��&:�S�>��7r���/�u{=��������/'M�z�{�"�T�������'z�qi�v�'�g|�P���X�<"��s��}���%L�Q���$.U�v�|�,�}.����'m���I7h��'	5���>y~3�6�w����nd����@����A�y�A8M	�o�*�?q��������w�$�)��������9T��3�����������n�������y������5����2��A!cvY5r�IUB9U@%UI;���2:8���L��3>P�eX��)���svr�����_<����6V�YQ]W^��>�9%�f��M�$��$����2t��gN��)*�A�]dl�xy����#o
�V��=��?{�j�C��-"R���u/m>R<������P�������:�GN������D��*�3Wu�k�`jg��SZ��o�I���u�C'�������S� L�,b�i�/B��y7aNh�j��(w�|;i��osnXzi�`�����A({��,-\�/x1]"��M�;��m ����Cs�\������e�g}�<��M��&%��9�cwp��y���`o����>��]�����.$���z�36oK���]��G"�vN��8Y�r��$��S�+t�������f
�0���vm*��/,�v�����J���������aj����������i�DQ6�&l�!�E�J�G�f����p�k%������^H�&(_0���m3l��^��V7��T��E�-cbvQ�e����IS�uU>��d������������0u'���fl0dXX�����qM���$�~�]��]���2j�i#v���jj����WV����t	Y��!��N�C���3-���@f�x>�^���w�����3�L�i��dP��FKv�;�����w~�}���Z�6�.}����7�(�^�%���<����N�[��*!#����	'���'#�&���3�&r������rO�Q��W���
���G������;���m��lH��m�;b���y��I���4I6���I�z��>��$��@�fg;�E<7R��E�����[&)r���x��;c���
�����~=���S1���v�3�(&v�fp����y����~��=�%gU��B���t��D������F��
zPf�����	9U�	��T�e�I�:�t��Z$����c�o���U}O���*$t����_���k���z�|����$���$�������L����2n�I"������;�
���Ek)R��&+;N�l�QQz^c�����Q�y7��2�I����I������v�
]�=F��,��U���WK��G{GtvT���Y\V��CZM[)�:�<���2^�%)5�N��I��d�I9�L����m2W��Y��~����7�X�������-4:2_�����$�v���\�4������~3d��6I��7T��FE	6�L(��D���w{n��2����yT$�d��WE������=���~�P/�]��,�!x).DDA����3�<0�1m��������G��:�972,�qw��X{����A�v��f�d����M�����+5���R�i���i�Dx����}K���C����V��� �m�N��N{`�����{�����k��l�=��L`t�f�&�x��}Ez���nD$�n
�����4����x\�Z(S�z|���}_*%v�9Z o�����{�q�]#���]�@9��&��
���]f,k�Wv��0��7N���so�!k�T����G��V������j�_�������������(�0��6
�������[�f����pz����V7�G��3]��z�j
�(G�6��}Ch��b�_�7K�={�iW���c�'�R.%��i|`D>n��n�q����YWP�c�������|�~�LmM�1����'�Zd����:Ry��������x����wv=�����7.�!~���:�M����w7g��*�t�����3u�v�
c�NhI�9v��R��E���qD��~)o!���ro���c�������UxAI��P	���Z�{�&���9����gTO���=w�{�Rj7�Q\������2���|0(`R����T5.�&&�������zS5D�z��b���������L��Nmn��~����u���u�3Q�D��[�C���,c5v'�5v�<���o��i$�T(eh���;i���hf�i�y����������z����cN��s����p.����H�7��r���������B�I��m.�I��@��H��$z��#V�e���K��.�����������)yM���>����v���������S&�3��3����;�I�;C �;���s����.ll����707w�H�l�Vc��N�iZ��B�Y�|�6�P���@8�M�$��g43"��qi;��������Ncr��
y���-�<p8�h�B�z|������k��F�z��2J�I��m5D��I9�O��'����
$�#9�z���o�Lm�-kN��'����V
�l�eN&�r�M��2�{@�R���*IC�����I;�$uLhd�$������8�3A�"���Uu�,-�]�1x���H\�2����j��7�K��;iJr��OJ�����}�m*@x;�}5���'v��oog��E" B/\������vO ���o��q�;�C��((���}��,F*"��S�U;���A��mV^m-�]�DQI=����s�uj	*%�}��}�vn�J��a��<���$o�|�y���b�����l\%���A�[�][�,��NU������V�E����D���L	��$��N�y2��6R�6�k�z�W �	�I�]���<��4F(�POs���_���`�Q"=�zs]����P�R�����`")������9�B">����za$A��|���S����K�v�j}����'�W#��45��K/��:A_v@&�R��o\���o�{�z���1�$W=�:&_�.��](�pt�hx����������w{*�]�Hj�e����<��C�;rT��g��qJ��*@�	���R a=��2HPBc�W����H� ��bcW�����>�Y�����rxB����wy��s�3�FY���21LYj����#���������<Vntc�i�f[��^�Y{�=�K���H��-2��
��A�DW+f�-S��<.xc�iP�����"��/GMU�o]*��Ym��������=-o"Ub���{~@���C�����
D�}�c��c_3DEvk�1����H�!����qy�q��@	:��vlr��H'��\W���N(!�	��e�^o[�3s������sE����Gr�/K�V$^q.�M�[�V:��"�zy��}���������;K\�_j7r)S3�'j23=/�u�'\�"�Z��I����U�Lng�����'�H�vF����)
�7������c^�b� )
�w��wD@"!
f������y���1�������&��"������A R��b�jg�I����{�9#��;�5s��Hyc��YMV�6�k�U�xZS��|�.�p\��uY��}��V�;��@C�z������2�P�)�7������O�����[��t����ud$���H�=��I�T��L&uk���|���1�&��/����j@������7�w3RB��{^��';�!$")S�}�5��H�Gw������""@M:��x��(������B�����s�U�b����[��f-������R�����ju�X���I��S�������X{EF�Fvf����l��]�uN�^�7^/�6+�A �@+m4W��i!E$Hos������B����nv����@�������y8���4�{|����k��(�R�"�|�L/z��@RD{������b
DD�(��s���U�E�%���);�|��B��]@f��q��	a9��z��o�0�g��g�xS�U��3o�+O�[��B�lK|>�]����`k��XaJ���K��s����y��������1�n�T�@��cY�a�B"@�O�������r�TH�(E�����EQ��t�
��oX������0"�A}���R����`#��x��������I��&S�=�\{])
�������$���&��U����ZMh�����b/Z�7~���,P��&
��{7j�o�9
�����Gm�j��{�I��@")Q����H�$����]�uTTDQ�.��l0g�����Y���(�����IB�Y�Kyt�**���o=����TA"@����r���g{9���}y��� T���3F�f�Q��){������A������D(y� u.����W`X+��C�lMa�v�b��9b���n�G���zL8��/���d�<�6��L��U�s7��;��9@!�#12�s��~�����1F�/}�m$�}����|���������DB�o�����!	�Qz������\}���P���=^�����sU��:�W!�����W!�����0=���t N�z��U�}7K�&r��i�@���&[�SI�}�)G��z^'�&���YP���n���1Mo8C��R���9=X%by�xz���"~��y�t�����|��{����"A1�����a�����������DQ-�������'�H��y����$�I7X���
#�'y[�vq�Fy����b�9�EC.��}�E��(/�v�l�2�8��F�s����X�g�O�j����R��H�����N�:�d�]�
wZ�}��|�����	�'6���cC���}`�)���w/����J!^����K�^�H�B3~�q�w��{z�tQ?-co�*���Fp^�z#'�JR�D�w>���^�7�c�D@��������d�*"(L�d�@]��A]W9��,�U����PK�������3�����f��"�V�-���|�e�I�>*�uXK��`=,o�����L��i�{�[�(����c�g4E"���}{1��D����w��3n�E'���\\�E~��{���R�(���^�&�@�7�~�����EQ[�������7����{��+H���5�P�
�z�����~
�"$""�����������s�n���K��m�w��!		@�P�Fm�6�v��ol�����l���z��B�B�����7��:�o��*�������V���"%(DQEs�����{�������9�}����t6Q0�x���o������F�^s���B�UP�(|��P���]�F�o���\�=�_1�k�����sx�����@E(�� @i�N�:��X�v]u/>��
����������6����`��I�����|���AH�E � ��w�=��n9��n*����$�;s��O)��-�1�>��'���<�G��,����N�Xg
���1�g��r���k��+���)�-�f�7�����$����g��z�T%"4����BP����}u��H ��_.��jj���	6����/t�$DSx�K�	Sy�;5[D D%��gNg8�����+(�)	�*"�Lw��}��^y;�gW����SY��$��	HDD���B?�U�{-����_#k-f��� 
@D#��������8��;����Yy�%������$�@?�����6�F�"�M� ��A")@�_e�^7����n�x�w�������wno���ERD� @PiC'Ckv��Q��X��TS��PDP��G=�g���bw���^����z�u�gq��x��H
�O���-B�c�fS�s������w����{�B)G�-����o���*�d��7cH=�p(�(BE�\�
}�y
�"����5'3�����q{5��l����Q�������aSHY��G"�aL�	��l�����G����l�=�S�=k��%�����/����}�w��	�����s�fo��:�����_oM�|9���$g:�7z�2�y���(T��~��kS��/��|�|�\��O}��)����j����]�	�5�����n!B����1�MG�Y"�B!$UN�6�_����bq"���}�kV�53j�p�
��
! �f�;�s[����]���\�������o�RPD
"��{�M��������EpV��,}�$~%��D�b�{��{��{�{��a�����+M�$��e
T��B�|>
D �&���{3�s:���o�T~9uty��r�����b�"�H�"B�����b����s���������oZ�=\*IA�Wg5����v�_���NLV����S����)H��V{������L�M��w17IXG�������A��S�.l�or�
����}V�^;���r�����k��<2dTQ����%���A	�����|O{�Y�O���������������R{2�r3R�<\0+dD��g�v�6A����>�����`���{�T�����q�bf B������gm�DVy����u����+����BEB3��(��5�o��v�
��w���������)B�HBDFT�My�����>/����y����o��W4DDT���Q�U(���UXytn(�s��v�
$(� �B������ ?]-l:�8���g��A$�(�
$G��k��{����5�����{�q�����B�����'+*�����B���)�f��DH�(N�_=��c}��v�����m��cI��+\�gL��^;�{Z�^��w7��e�w�R%�@����P��x5�[9Z�^�5���PAAJD(�c����/7�d���mm.'n�nO��I$~���b����_en^$.�������"�w�\y������Jp��s�p����,�ck�C��w�2T�o����2�-�$hv��;�sK�k�T�[��U����gEU[���;����!H��o���.�)^��w���{}��D�;�W;�^���*#:~��E`��33:����a���JE!�����/_�""ED(%�3>������5����s��=��{���(��!)8�i��J;��N��]}������_'s}���
��!&1��U����������
���@|>�C�
��+��vs�HMml�.{���8����B�@
�3�q��;3{���;�9w���{�E@������i3�0n'�:d���kb��wz���I
&w��~������~�Nz���B�"�@k:�c�u
0*r�;]�
����N��@ ��"�,DDM�w�ow;��g�������}���I"�|I �3��O��K,r�$��	��=������<����w�I��"�B�G�X��2�����Re{�����\W�wr����E��	����������q���R�?��A�}�a�� BR5�1�]�_a�d"D�G��{2�D!ED���w��������f���3D����f����s���k�W �E&.�,�8��T"��|�~���q�c
D��%*"�D~����ze�3m��bs��\���}~��[�� �$J"!DE	\��n��[�����a������7}�uD
R����g���Y�^8�;��3��K�9��H���Q�Q�������R���4��cCk����^���R"P �%�}��z��.�����.�o*0����� � �z5��U��R���W��=���=�����)QA�����Z��3���a�L���K�o$UF1��W�{|{����o3��3/�,�s~9��1Q(��������+�{umrA��N�':��R)QJR.���|�ag]��$ctp����V*�������e.�YZz�4:�D�N��Y���(x���o�Su�*�H#gkVQ���o�������\�-�g��Z�+5�;�q�Q���������)H���ns��{���D!B�E)7?k����B�H"��`� ��H���W�cq
D)"';�����" ��N���������b�AQ
DW'u7��}���77������s{����/E(E	JD�y��{���y�^�vg9�+�����>5���u5%��H��V�;Xv��B�)$HB ��\���v��v��^��S���4��L`�H�D��	�u�Ql�:���C�`�z����B�PR��g}�97����q\4��R�w�\�S��Z3^���$�
��HG{��y|�c�~�7��������r��;��=�����"�~�{{fY���.�,���,rd�9��5�ow�h���$��<�7���ou�� {���C�� �"DD��	-�;�q2��l�5��"�w0�B\�Vh�2q�{��>6Y����j�y]�Ao����t����Y�8���z)��{�v�Q-K���������v�=~�o�,JZ�����E �������/Z B=5�gw["�"��}�^�������(�w/w._3u#g�:{s�"�`���<���iADQ}5��w��w7�� @���i��|�J"�"�E�;�s���[	J4�����}��H?I �'�A$�'�2���m���
�Nw:�5~���7��{Z����"�D�>
����&i��sO��[�^���}�"*��}���9zc��3����������������AP @�

�����n��+���t5qs,����L�Q"����j��{��u3.�u�H�����ev2�p�A �	EQUA�-��������������f������[�q#�A$h
�L��.���zu7����g<��BER!
C�0^{�v�b��N����@�� �`O�> @H�"�[�|�y��2�w�b
E�W���.O.��%��cuF*��mQ7�����R����4]��y��I���n�n���<��1b��	g/D�]�aL�]��8Wp���KZ8t&����1�#��""gw{�-�k���* �E6���Y�@B���>��}��" �D""F~�>�m D��MX�M0�!D��"$EBs�����B$" DE�(Lo~�!y��B(�);~�1O\��&�n)"�_����?|
n��',�;t�L�����!8M���Q���:KE��9:����gG��P%{c��+����X,
2�S
�u�rhs�������[�X�E���x�n�>��dne�����i76��
C���A�Zlv������9��*�y&�m��N�2��H��T�(�����z��.,]5�����&l6{��e�\�/'[��3�m�2�G�rv��.�<�-]�"�8���\�fC30
�V6M�K��C����Y����B�|����z�xG\���q���U���rD�v���v�1zm������g!�6��a��	��h*|37]q�#�=���\���������"�>����Oji�tGV	��s�����s�A<�&&������������c��������Z�qA�%-�Uu`�����o${���>��[�K��8��6)�U+�<�����F�1��e����_+���B�xf�d���H��i�����*�!��\�P�X��]i�R�k�#G�5�GUm[A�����f�2��\�8^x���-FU��erU�@c�n�����������aU��}|N��"��l-7��7q���}\��h��:�4�c��'[j�����XV$��g�G��u��Z����SW��7��
�j��m��
;�|�n�`)�oa�e�2�P�s�M����fMD.��/�Pb�G��c�������$G�h`�32�2l�2��iQ�P���C�^�YN&N\���7�,��!�n�k����w����������/R�'��V��u�f��� �S����,�N�	^����k�OtHM����W,�t�����_9�!��	�i��p),��i��3SY�,�G��Y}�x�B���� �`��Mf��(�X[�F��p
 ���e������������u�E0�q�p�B��
K{ �8o��R=���.�f��E�O�v�@�{A���EX�Hq8�%���v���}t�o�J��2��t�:U��L�����vCi�7��L[�f��������=$M���/�4��l����8���5��s'9�r���$k����Y����� C����]nd���4�8jW1u5�f�Q�v�mh�"{�u��;��[0s�v��=��X�d��v��j=y�1J�M���W�<B�g�U����������UI�����w-}��;L�0�xq-z�C�5����4��r\�>Z�#V'�����s;���*�c���p������v�����M\��9�&e<@���(�j�Ci��+��l\W�p�������v�9��U����S���Q�T��QD	��E����:��;yU������9�[Q���|��!/Q���#��b�SMiV^��X�]�H��3*��[8H���-��.�c�)KGKz��U
�V���Ul��nI8�
	
pF�7�� �x�b�gM�$�Nw�+��vd���@��w�s/��RN"���]Z2�����-!�����=��&{�&��8��v����4�������O�v��Ra��3����zY�d^��IYT�!��{x���y�j�1u��H�[��ky��Z��wG%V�7_�m�v�eRAf� ����-�A�R�-��#�_����2m
�-����^M���!6�`�x[�}x�E���7�����v�iem���QNw{�����s��h/�����y8����<� g���������;�fU��cE+��	N|,��u{�'DX����:�.��f1�[��rZf���yW�I�P��3T����$ ����'�����.�(��J��>��x��~�9|�,3_�;���m�����[��q�[�S�Y���������]e����,V|d�����8������g(�@hJ�s���<�r�m���
�]��C��l&�i���!|x�Oor�+�p�+L�9j��J!�np&��;e
�u�>Y����3����E��p�9���$����n_���C�G0��^f��~gn��2zSx)�����+���g<�~����'��<���o_E����97���K��j��{(�,�|�`D����k������3��~�Y�{[������{���Fw�&n�T?xv�]UxY�=J���]��5j�u���U���6|���U3:o�����y��({�
/K�(�[LLq���x�!�+)���^���hpn�[�Q�Z���.�qAS�Wp�r��=�����y���j���0F����>Wu��]Xa��3O3�=D,��T�_	���T)Q����kVW�����v7dX�Iu`�O��NR�u$���;F�/����N}�[C�%���)����������`�o��>�?")qrN{�JT�l�
P�"�jGz��D
V���ue��#L�����x�x��<:���p,��z�2�����}yu��||%v�Z��D��{����v�?;��6�������otLZ&3�FF���w��[I�����s���;%�-^z�q�Rq�nx����z�N���;��H�{��x>-�Pr�g.�3Tg����2�d�1��0��������J3���=w=^z�^7�K�59-�y���}r���I�dU$���6��t2PLZ3�&Y��s�����������;���qoor�����VMW��[���=�����1�L�C9�L�FM�$�i'V�M�d��L�����~����:��d����LS�'3+�����������3���w����&�f��������otI9������p����
�.���U�����2��t���5��`Q\;V���}��U_���*�i0�fV�n�L���l����)�r�K�!����]��0�x������3a��\*�R�0��y����g$�td4gj�	��IMY>3d������Z���F���]��_1*�����=id��ek�_%���o�y��v�5�3��;�I��7tI����9v�?���;��|���,����]O�'a`�B���S���=qc�_�/���J�U����&�6�j���h�����<���q�W���&����\zb�Z"y���k~.qG����ni4)�]�/T�v���I�S't�~�o	?rQ�DuZ���w�1Z�P���J����-b�&�B�e?-����p�cb�9�����6�������3�M����.�/NE��
�>����Zb�e�=���oNeF9��Y���2�t\�����o3F��N��1UbuY�=��9�{�5�!��(���p0�����{Kip�s���Y�
�����{��Z���Ul��Q��Mk���c�A����{;��(��5|����z�p���b���e������^��O������x��e����������/�B�R�����Y"}��,��+e*�K�m�������Y�73�-����Q�p���}�Z��F\�7�I���K�J���z��;�`s�SN��G�1���Z�`��jgH��n�6�L��^�#��C�X���q���YX$&B������K�S�ogwq��+����9���J#l��N���*��9]4�E����Z�.������p�������<��8� �aEo�;���@�����o�w�����m39m'5L���v�E�&���FG��O<���?}�Z��6D
)v�v5�U�����X���_����y=���r�!i���n�$��e���&-��:���������KwS���$}/.L��LD��UK������"��h����vHTI';�������3�S/m$���U��g�=��fd�oM�������Bam
��/CJ���[u���:�����i3�D��E����e�IMO�5d�)��W�J�%���3�,��%��$3v\}�<����yS����\�^��9���H�&�qD������'Z&ov�����}��yZ����r��E���Vi�(��%x�Xg��X���q�b����U�����w����#C#�&n��
�'��.
=�r*����
R��B�R��:��wf6��6��c�/�Q�v�&sT�Q��q�1�&;i�iV�l��������7���s�G�W���B����k57Z���^�p�GQ�z�����V�N]����n�N��;td��2wC'��59?�y�����Zu7������=���������;�����f�~N�I?P�$�W�I������]��h�Q��}X�N�j$��g�Q��z��M�n��(w��p�����mD��d��7�VL�dU2[�#���h�h�)U@
R���/��D�Z��D]�Ow6��3,��Cj�#�I�E��^�`���Cjo|��A�����)�4��w�G�i���m}��`��~����e�Z�/@���]�zl�u�$����a�OMh�Xgz��V���
�9���v�d��qBw�.��~�����j*�F8�����T�"{�xn�������}��	tZ&�9��7T�x?4�)x{�{��`��/0�@{�9����W����,gz�����M'(������v�8���9+|=�X]Ft�����
Z'<wm�wxv��m���
:c�Ux������;���+�>6.��O�������z�n1t��������0�w�9�o��������Z���X�P�Oe���@)���:�4&���[�����	=�q��W�Pv���]���_Uv����U�d��7:�)�����m�<�m�KVI������;Uf�I���n�N����v�T��F)��w��7����~���m$��Gv�n�'A5���@2N��������w���L5�\�]��X�5
�l���7��m*'������E]��D�E�D�D�RN�����n��D�{�Z%43�h3�%�����&i[��Fy���8�����p�n�w�S����{�I����I��L�m$wD�G7I�z�M�J��LQ�:�)d��3���y�N�L�F�w���m�1��V����I��$���D�)$��d�U����|n��"�'���P�ZLd�k�v]������+���6����H���;[�y�7��Hl�*n���g{��z�N��#�I�'���G���^�������:n��	��4�����U��4�l��U��=_S�����GC&���R6�N����O�rl���|
"N�4�qV.Q��:����n�xE*�^���^Of=)=���'��������Z���LU$�S-��8	�	;�I�FNy_�VfMG�%yA�\Q��H�C(d���y����.�ex���k�n��Ls@��2wtM)�;D���;�&r�$E�'��b^r���g8oD�\�l#R���u�7�����M2�31�Y��7�{�����>�h�h�����Nj�
3���v�%Q&�����������}�z
oh�p�'�3���i�j8%���VTz���:��6Id�@���S�N4H�L���"�l��,x�{���<o�_&�j^����]3�!��x^�H0����=z�0���g����6�����cM?�oL�H�����8��%e�����!�r���_�6���fu��"��������(�k����4T�A���������+�2��~|�_x{V$�\��f-����D\�#��Wx������;D�T�~��;���5ga4!t���:�`�S}�����/I���1���x>�
cf9����5;8�.��T�hX�D��aX�=T�w9�bz���a������$r�I���!`s�AU�V��(�7k��63c���[8;kn�
����rI3^�;������_EX7J]E�Tv��#D���k��cV�=zu�y=�f^�����g���7�S��<!�����������kNn����$�n�Q��#h|�/�x����S��7����"��3$����qk�\�����������������K���,�I6�3�}5^�'��F[i��&6�J�S3{�����0|�������-�����';�����`���FL�K�;��&_�N��d�FN]���jr�I������;��st����6��]�3�-����^n-����z)S��8?4]�g����;�����g6�3�I4�i':��-3wFom39�&?}��{�������Lng�����u�GmW���j����W�H��+�q��o*��O�{I9�$�/C-S3�I';D�wVH�Pc:�&�E�����Kv�
����Bi���������=�,c���.����I���m&wT����l�z
� ��|3�s�&��[�v�������^o\���������Kx�����&�`�����:����_ER�4��S;tsv���M�����������P^�s$�]Su���h7R.����n!�Z�7����<���I����i�n�K�����GBv�d�:Mn�X�>}/��\�J��r�`�[8�iA�����������5�:9��������T������ctd�i��i:Rc�&}�w��s���o��k=g��J���M�i������1(�N��c/�f:�x}I"����&�h�6����S���D��~�����US��wT��{�fx"�
�Y��,r~\�-���d0�Lz���<Y��]���n���fs�f����RB��T�_z�m��G$�Q�!�-�(Zp~�8�uX#;h��=��"����H}g���c��<]P���+���c7�����^#fV�GK�oEe=~V&�Sqf��[O����s���
x�jG���y���7���{�zN��f<H������1�b�=���8����A�t2Z��<�
ug`w��'��ss/fOwj�������=.��=j�S��>��9�<7�	.�6�����wq�$���5��u?xxo+4�^#�����z����$%��5n���q�K����<���f
sO����e���$[~�W����5e�:�����n������b������T�������)v'�
�������������x�����*�A*�3+2���7m��_I%����s;�e�W4s�vX��v�6���)��<�����b��[F��(���7Y���6����yAN���=�0��kqD���N<�0�''V����y�u���QF�\�xF��1,-���=4M��Rf�I��3:S6�Iwi$��MU]��_r���{/�,>q������o	�AYB^���H�s���z��=���N�����e�Lv���I����I5d��$[]	�I����Rt+���WO8o���B�T�����x��-�6}~���]�Nhf�3:���i�S:�;D���{�<�2�7����Q3}]�����<��8jN����TMa�|2q��'�����MQ3�2����2w�K�{��i���e���l*��+fgtA����9�������ob��B�Z�k�6/~dv�j�m��;��S%�SVO�]� e_r��m�m�����a���������_;A���n1j������~��h����f�Ii��@��$�$���@�}�� ���-���oNX�L���*z�$��L��=�^��I�4����>��'��'�j�6�.��h�����?{�{�|��n%��a+	�~��*moD���M������H��|��G&���+�8$���td���;�'42oh���>f������QZ�GQ����Y���7��N �}iVc��>�3�}�g-����j�.�'{�gj�M���| ���K
�l���y�
�0�����D���N��
�5�������������������1�I7�&]���qi7�\��w��g�+w@���0lb��[���&f��wkL�����Y�;y���/In03h���:����[|j����Zgd�~S��wm*B��uZ��	�uMwL�
��y�W�D����������C�;��A{�@�wo�d��Id��Ou+������Q�U�3r_�<<�5�t�98��=�9�u�"��vP���I��;��!�<�l�����K��������F�e�^��7�3�����`��=wf����:����D2�a3Y�����Z������,�M��K���h�n='%�!�J��;&����mZ��5����-V!����5������So��y�i��It�����CR�$�f����C������	N�3�������!|���S�=���dE@��>��v;�����c��������B��U�hUA�7m��\!>!���.���Zg�����aKg*��o�z�����IR��f�����%������U'
fv�#tL���05UU�@$�W�y���nU��Y�%sK4T��-��k-�����P��4��^����s��v���[H��N�
�&f� \���&�$�3{�34���^8�"=���3�������=n�~��j:�;��tb?n����UP���_��hfoh��D���[IJ?D���K�\HOY.�����\7�g'�{���sf�-d_Vc�^t���I:�L�����'m{���H���4I�Xn�e��E���
k�B�F�i,mK��9/���hQ}�bw��~��������;v����m26�3�L���o�~����|���1��&���69#[���R}�l�������9_�~�3[I/C'wFcv�;�2���I��N�L{�����>������S+��Z
'zrM�=�n��o_k�}���3�I�����;��A����$�G�=����^�t�)�C�L��yF�/\%=M~�m�]��������j�����L��I�����z��'�v^y����}��Wn\���=+i#z&\���W+�����yR����������9h��m2n�dj�9�>��$�
��Ena\�W������J���ua���������f������9����~���|��Lm�U�������'�4I5vH:��-F*�����f�h�r�_|��;��XNUHg�s���p;�[�{f�����d���w�������vkNb��({Y�m�
��D������Ldq54"������*u�q�k�
���Y��F��S�����0
Q��$�b���[������^�}h����{
�	�[�!��S���S2��w>������+�{��}���F���<{���t�or��5��h���/"d!�x3�p5�U����y���"Lu����-J���y
!�~����7i��h'���PI�����\��Rr�vF�M���nr=���e������B��|�]e��S���(���3&7�����w��2�^3��hiK�-���j��7��';2s�YY���y�,I��58(��x�H\��6��e�s��~�,��r	(��$�����B����v�������T�6���oB���J�����1��������V�oD���NRL������7��T�C�(IUU��m����^j��������f�R���k'=S��qv��W�Z�}��37T�uD�j���7v�E��=f�#��V)g���j�W(����S��=�m����=�q�;������L��N(��L��N-$[IZ2sh�w�����<��������E<��������,�������<�3�k�����s�g;����v�3uL��3�I�h�te��s��{�>�o���3�W-'F�/#?Nis�NuN��w�/�y�>��2kFNuI���m!i��$���P�t;>����w����2����9���<p�>��r���9����e��������Fv�%�L�����D�&kD�������6l����kx���iu�J.z�z�e3��Pu��N����3I"���iLhd�I9�3v�$��b�>	v�n�*�TO2%����dZW����z�I��:x����B�.��U^�37v����tH�c�L��z�� MQ'���X���=�C���hV��)\T��<s��^�Q�Y���F|'�w��~�����>�3{�e)-��6�;tfw�K�d�FN����p�f�W0��������{gr��:><���HY�o/����zQ8I���2s�L�B[���{D���2UW�����Pb!���5o}r����fb�D��@x���	�du�������y|��^BeF1�����������X��bP@]t^�7��c5�= ���ix�����Bvn�{��w���x��O��wq��v��;�a�jf�>����\J����v��/�������(�=.���;u����[���3!��Q��zg��G��]�Sf��@���o]�K����W��j��}����I��9s��������|{o@���x)O=sA���%pmL: 'T��]24h���.N�y��������s.f�u+��H��gq�s�m�K�|��gn�����c���XU�����+��v��.m���h8�W"����������5F.�A����<^[=�����.�$��:y���lp�l�d��N�+,em�n�Q=l[���Z\%����^R��%c�����3M�K�oo��B5����K���3������y��d�	'j�JgTL��M���������|���g\��uT_(Bq�%�O{O^��X�WJ[��/������y�~�/�.�dwi���N��g{�3�������v=��}��������\P��ev��Q#n�hK����
��k�����4�~6I?�'���&��e�L��I�d�i'/����]���3�@v���[2�p�9��U	��)A���H�]{%�:�w�fw�'{i����FGT�t��a�MU*�1r�^(���X�]J�b����r��DF;��u��V��o�Vg-�Nj�u�M)'
IA�z�K��Be�����w���>��oA�^,�������Y��$����	���sX�1G�b���`��u*����NuLP�i%����d���u<�C&��r*["��5]�!�N���|����n��X�1�d�|o4�S&�#�L�&����{�}�_�����?W�kv�t�����]<[�OM�(��,+\��t����?7Fm����5iL��kh���I�����6��������3��a�S �;���^,�>%og@�O9������[L��$��H)1h�n�A2�g��_�?��w�����������Su�'�^�����J�W���|�\~�\�����Z2�[L[I�*�n�y_����k�V������L�nU�W��#{��Wx4y:~b.-�*�m�?x�]���W�y��k{���5�S�4{���������:��_���x�h�����%�����v���6��`��m�UiI�dr���}*$�����������0������$C��q"�	�t����`��.z�D@nA�����f>c	���%���J���6���������uN�����i��G��R������B��_W�.��|>q�����q�F]���mw��iu9����6"9���>�e���Y��g��7��kht��J��cp�)A8Uu���t���m���KE��^s�q����a~��}�%e_��C����m�d;[	%���p���3N���`���t�|�.K|Lm���%W
���U�*}���V�'�U���t��Kq���f�LT��NK�{P�2G��=7���*��u���Y:�|���N����O����S0C������^'7�����{��1�I6�$���D�8�7�L���L��zm��lyZU:�e���l��5O�����Y���q%���d����������mS'.�w�R4&m�H��m�8�lf,�Eou_j�bw��i�
:�o�����yQ�38	-�"�'v��&��]U���d�VX�a�y
��=�b����[$
H�Ug��q����WU�����}��gwi���n�Lv���}SvO��$�O�Z�+N����.��nK�����{w@���eyh��>�9~G����������&Mh������wtH��Ff���s�<�����V��J�<�Vq+k����NkJ:W,���Z��QS�E����f�������e��	�IJd����D�\F�ZV��7eG�N��s�M��F���F���8W�z�� Y$��T�]I�FK�0��I'�k��z���"x9�����@���:+�����}s�z����]��{��"�Mh��I:	�������9T>.J��*�o���U�\���	�-���F�6���<�����'�8��{��)!i6���sh��imR�':������{���~�������i����L�J����PX��
7��VV�$1*�W��U}Q�P��$�Ff�-m.���YK�<�~>���P��c��.���R��#���n>�*A��(���f����j��:��/�1��������_���6�n���Fd�*�Es`���:��ld�=�!t�X�{1��:�^�uZ&��:�u����A�%�n����Y��ae�/r����N1���������������n������Y����5xx,����nFG{����2�M�{��w���<2�S/,�7��������>��w��<���
������nNB���3�>SH����~=.���U���=��^�<�����'���~����l�D+�4
��8�l8K�zH���^�k�X�LJ<h�Vp],&�#�{��Q�v��e���1�
���sR	����a���F���W�UD.r����-�Q���
���M^��D��[3b�M<{���#P��("M����%���+j�]U�:Wi���2�pQV�)��c�$��m�!��v��T�$3}�a�&�v�����&�D�m�Lm�N��$��`���;9uz��bz����w$-����aR&�<�bA�g��{���i/m2�6�3v�9T��Y �'�n��`���C�����vl�-�b�{�8Sl��5=1T5�V~���~������r�s�2[FN[I�NwD��1�F����<����������@��|7���!��2���ce�h�|�C_N��
�T��I	-�#�I��$����3�=� �������SnT��z�nx�����J~�=���D�xyF7	9*�3Z&fU����wD���}wVO�j��B��:}co���������M�z9o��aS�X�$tv���w�����~�cBK�'A��N��4L��':9��=�����{�+�����L����N�'�u��ls��~��o�����������%��8S:�f[i.�$��3����q�B����N������������I���,T+2��Vd������$^��I&�3-����g7D���RUW����h�~�:�C�M��~������-������5:��]	[I:��tF����m3)�U|��Jr��,Y���V|2�n0 ��5��^T�+���.,x\m:����}�_�y%m';D���&��N]K�$��?�'~�98���z�����^�cn����������lim�y�0c�t��Oq	��� ����{��sry��/�Ca��(�o����w���Q���������}��3�����{.����J����kY��+4��R��{�wj�*�Q���;��_�|����u���}�oy���T)"j����V�]��3������wDu�?w�����B���\�
�]!��
7�<��^���v���;"��C��	\��x����^a^��yR�/�j����
�^noEDU#���>�{��E`��nbbA	}�}_c��d�@������o��V� �1|���y��� �P��s��f�\�/gu����D"1}�oX���r]�w��-HU	A�>��Z��������lH������{av*�HW���������m//�R
�,T�|Kw8j�������;j]j.�/u.��5o`�P�����/3�}����6�*(^�s@�B&;�m�O���@)B�p�U��O�g��,Ec�}������E!���y~�����o�^��hT"zg����X`�A<�����h�y�8OA�d�s���X*z2Utf
OZ{t�P3����G�3���7e�7������v��D����wuZ
L������!��b�y��h;�J��dT4 �}p��nQQD��sw�����4(��1�N�DT1�q���MQD r�x����
JD������n"�P=�k:�=��B����	���`��@�
��=|�M�6|�5�*�i��w�.��+����.�\���,����Kx�u�g���WC�HU�u��Y�����=�M��F�lM�Va��S�(7�:��_��""=P��x���
"�5�k�����1 �E}�s]�/S�������o|~����)�gg�w����9|��F�qj�U��|-T7��`�!��^�K�=>��v��8�[���[�����+�R�%��*�a"�7rO>����]��Nt�l�Wb�H��R�d:z��v-!*�
��6�g-�Y��^�p����Q]kk�������B 3�|�+H� �kW;�_2� ��X�\�j�$&{���S��Z��u7������q���M�!"��y7��1��DV��1/��� +YxV����6����@aK�*k�w�����'k��(��6�������79c��q1:'���4�ro�,����.x�������|e[�K��W��oY���g�]i�*	~}�sc���BD���/�������y�T��5y�w����H����o}�������R������$�Bu��o���6	�1E\�2������9��{������������o�Oa�xY��#�F&N�����W/\w6�%����1���W��o]�/f�$�c� �W������Y������nEJ�y�� �AJGg[�y�D��Rm�}���}���`�QB ��r�|��)!T�s��J�a���I?g�����o�@H��!�c�Z{l�j$D�R%������I ��5����`*aU�����[��
���%=��^D�QU�O��4QQj��x��jE�M�DXS�r�P��@�����q��]y��������h�����38���+�v����Cp�z=��c���d"����}�oz��"B"��^9y�	(�~��k�k\�$	��x��*��sw��%"(��=��{O��["$�H?�P��������\9(l�NU�v^�Vu�)*l�HM.�w��������:94h�Y��vl����Z��k�%�+�����P'�`�����'lJ�l_���E���V�u�-�@TV*3s���.O4�����������EE�s����r����������kQ�a��}�}��1X������se�Q�w���/������$�Ai�����9J���^cA�������7|c�ul'/}�_3���X��-���q
���������:�Gkl�Q����|��l��4�X��L�'�;G9D�8E�����T:{��}�p���
,M����AQ����������bs|>�y�o.��P��/x�}w\AI�o��}�uH���gnk��nri�@"/��\����T��Bg�c8��/,Q��k��V�����ZF(��(;�X�����.������������JDr�\":v��{g�f��D��B�K�u��[�vr�QR{G�_��g�y���AE9��\��c6�fAQP �1�����_L�P�����^�����|�o�{��k� (Uw�wy��8�&�D
HEw�o����E
Q@��o9�o}���BI �I��DH�y����"�3�sUF)����Y���7r�2����'b9�����u��c�M.����j��5H��m�O���*��
K�J�k:��]�� �DR&�v�uX�TH������j�H�{��&�H>�����yX�v
U �#��u�}��as��H�}�g}���i���@J!
���vc�w�"��y�����7@��^`J�/5_9�1U�([�>F.�aP�@$% SZ�/9ae��4�^�G�K��c(��'�I V,T�/�����/o>����M�l����9�7}��d�	AGu�9��u�N�{��u�N�Wa�-2���.r�32�!Q�{s���&���U���h��hf}2���'��A#�7��rPi��y6������ ��I�sot����Loy���g��<�k#1����������������w������C�]���|>>P���:*"P���������"��)QAQx�wS�9�]��d�����{E��<��������'�r����-�����C�fvJRc�d�!��M��mg��	��������y���.���t���P��n@�J���.���p�1s/���������E��}���;
�J(�����|�~��R*����^���
"+����V"��#:��oz����B"&u�k���y������(Q���j��;��(�A$T��Lt��P���,N�K�I�1����'�o��_k���o2��^���y�^}s��� �� ��fU�L��U-������1��y�;"�(R�RM��gx�����w�F���2.��Y���5�>����A	��2�ut���fU���+���1�w1JPHD�I���7�o��o;�i��=��V����� �H{� F�*�����g$\�����������B���3���3u)���GN	���d�;P����*&w;�N�^�6�;]�}�g{�������{����(*�$�A������R���.v�c1��L�#fNKR�V^��\�����i{�4�/��0���z��a�����O1Mj���eo���S�y{�J�Un+1roo������?y1�N���O��K�_7�o���(�gn��V��DQ�{��_���U���Lb���XTPH!^������'o��M�si(y����u��*"E q��,P"(�HQ?���7U�T��<�
��M���[�H�DHA@7�v]eP�%�
�������B�AH�*1��|s.Om�K�x�<�^���;�s�!RIt�ud�
E����S"�An���H$D��$D���F-
sv����F���L����A������b��{�����;�'w��.������5��Q �I��r�g9��w��LC���J:����e��!!=����������T����+�������A��H?A�/����k�xoMF�.U���"�!!�9��y��{��T��sT�%�!|Z
v��n�r�]�}.g{s>���;��1�UB~9^��w
����c�.�z�9�ks��k#1�?.�*L����jX��JF������/|���I$x�m�TA	=���>�q��"Dva�i�c[������ �P�^���=k�2B$E"zz����}����M}3�cR�J)}������"""/|��1�����B
$�� �`�n�o�]���.m\��OQQB���w������9�o��_��4�<��3��� �����}ghmR�d��;{C,�<���f����" )��w�w�U��W�M�e�8���sv���B�@��	��;i�u����b���Sy���Z���RB��|"��������pX}v�(p�� ��N�:�������������+�)
A"g���W�j�#��]�i�i�f�3F>'���
Nu��EP�%����c��K�51�$A
EDC<�9�x���&
J�vw�b��v$�[��vY]"`d���#���q��zF�*��9���XS�s�e8���k��_eV��]V�f*�\;��� DDAv�Hw����A���{;��	
�#��;z�q���
@	�1��j��������ED��������D�!N}�q�����c(P�A"Lw��1����QDa�n��W>�����R1`��g����>���5.���w�]�(������[��uy��51v��j�>KO��A��y��r�f�;��o[�w+&����N���:	�!�!
�{�3�4��j���kw���fS�	�@ �PT��S�Q"�h[�gM��,&go���b��1H�AC:�~�x��
�.2S�k�f�6�?�$�P�HTD��w~����]�{��|���������`,Tbxy��c�(9��v*���xk��C�B�PJ��B�2df�F����f_=�c9�c��� "��EA���E�%�;��y�,V�e�	k3��_��.Q��g����)�<=�Lbp��K[��.�.F�y�[j�>�#���)|�qM�w-W
~~���J+
s<�=���B
�{���H�c����&1�fB���{���� �_/��_u&`�RB)N�x�98 �9�w_o9�$$R������+L���n��2*�#�|���v����^G�A5o���{��?I?RDDD)���;5����M^����I���w���P�P��|�"E"�D�7�_g��^��{�\�k��f������uB�D��&1���u6�e��
����A$HT"%'�-z����Mv��=��������f��$��f31�x��J���JiA0������"AQ!;�j��[�k3^�����=�b��]��g�p�H�B+S�b��n��s�o����t���  ��$5]]p�'��y�����g�AD�k��fn{���Y��*���nBK������t	�8��UoZCV]��s�N�6��FE�����oZ%-�A��f��[W��w�lN�'�������^vX�O���N.�����""�R"O.��c2$�H�{�����^g�"������_G(��Y�{�1^� R""�����ARK��w�{?r��VD*���1�����_n�w���e�"���b(��,���/xHm���Z��VTz�F4����I1E)�$;z�u;�����c�fno^��q{�3����o�����>D�����������|~ �	��_��1s]H�m�\���Lc=���]��w]�'k1JDB�H�P��8����9�����x�c��'�)!P
�[��5�&%�.�okj�:��}d�A�H(����w:H����r5��<�������o���OgR"	HD��������yVr�y���abQ���*"@�HRLz���X�oq����^��cSNg��w��]��H����y"�$�:
���wX�S����e�S���P:�5��p"����J@�7�f��|�=�����>�<�J��Uv����F����(4!�,���������2&�K|��
&���2�����G��L��(����a"#�����nC�UL<�!�N}�}�{�OQR�����wi
��r����HR$ �������(*	�P��>�9.5�����m|[e�3�{x� �)�(
	���;��2�;��(Y�{� ��1dE"E��W����������o��wov��������s��k�������$���Y���M���h �"A�e�����e|�g;1a���z��TQ��T*��(i(Yn���v�	pa��.���TRQ&��k:;�����A�W�]=-n!�01b��I �	�R)�T����c�cZ�a�������������j����TEB@�	?�������	l����\����Y����@TDYV��`�������3ra�~����'�'~�L*������^�;�VK�%��<�9�������W��2,�����_8��::����B�9t��f�7{y�����q��V"#��PV1����.�{AE �$b!����;�5 )H�D�V��b�f��EH�!	^��u��zR$RRE"O=y����4B**
%L�_sZ�x D*H�A�2���u�w?wU���B�!�{{�C����@I _���A����U�-F.�/�����2��r#��}�K�	�i7Z��Y��9Q��t���Ue���������.f��QT����$�	��k;��`��������k��u���>
sUw�����2+S��W5�n����J�j�DfsUhW2U�P��t�N,�������������:�qw
</id�����
7Ke�h�fcG{�c6�e�:V[���yW[����0q���&����R��
hf�������0����F�v�a��4�|z�e��7H��V}1�C]���C�|��i#(�3Un���K��3���lp�������.k)�s���7G��8��[���z����kdW�����Nc��r�.�e-�����)A$�*�2�U��N����mL�����>9�]H���qm1A���ne�����)��*m���@�pzf��5�F3ZG&5�n��+~���N��
rk����+c�-�Vz��h�i�n����������W���
}x���,hx0�����[�9���3�5�Mi|�L����w�Ueu�����I�/;��_e7�F�������q$�*����&��pJ(N���X��w{XT ������9�����(]gZ�+�V����%�kw�wJp�E�����yl���sC ���z/��^C���u���hc	�'>�����r��p�����2�
:�I�Ia�g����d�������S6J� ���^����W(���q�E
��=N;�OMh�C�������������4�a�S��p��V�	���a�������d/���cy^Hv�Z�q��2�'9�WBz��+0 �f����U�t���Y�� l�X����\���rd��S$C�O#�1�����0EQoS��,����[�']���Y��$	�n�c&�8t86�:<*�H�����g?�	5����W�f�W��W8Ql��e���h�)��r/}�a��_���eYHS����[��e����hf�$1�$�q�S7`���I�	�0p��7MP����T�6f������_Mm���gR�&�Qj����xOs����U���f���U�����{n�_�go dZ�J:�%ee�'�:���Vx�qm�3�,��^��i��}��p��H�F�Hh`*�6�+$r�Kvl\���;��1V%�L���A�miA�������L4L&���Y�k:����	;�/u�
K���q�����p�A�����b���������a#��]�h�mm`Sl���z���y�����`���kgpbn������u�+�9#���XMgq��^�xo��0��/���������s�J��#�I4�.pRp���]����v�wO��{�9c�u��2"/.R�!����~D9����{�y��x�Ce�
�L�N4�[{��.�t�%� V�wM��7������S�������+���z�%��V�b�����7w�(�����
��}���j������9�ik�1��j�����a������ P��A��_u�9X���U�F�t
���I���d�:oN��L�%�Z��
�e��*���:M�!��d��c�'`��A������h���r�m=��.��{luw&p����];E��h,mUn���rV��3$Y�}9�v������L��kT���ka@�-����@����,���
f�y9�
'0��*���XPvgj�Z�K��
��,od����F
��L��b�*�vV�����U�v=�2V������S%���
�o&l�z��/;62M�*Rut,�wh4x���C�e��p���g���������u9;as�O�R�Q�
���]�}�sh\�g��x`��LX_�&�
h���#*e��u���86.���F�r�1I��.�j5�'�,R}5G;/X����N2�g��bw���������;��
����K��;�q�������%�u�n�u�Ar����|����eN�"64�q$��I�������s�=�8//�k��l�8s
*��}:�t�wY���\Lu��2
����m�"��C�wX�����C���i���Z4���c�0�j=������,U`�.RBo��Pn���:�������47_x]<2�P�O!��L\�o�s�TB�yjxz��T_7�������!\wD}��G�����Z2s2��W���Y��t�e��q����7km���������^���[�$��=s��n�������8V�e{#`�v����T�#�W}#W������"��_.B��h���sI,�-r���)~�Q��[{}I�����>[����x{m����V�[SCP3��N�}Fo`u�u7��	Fy��;E�'��E����X��J�k"���}��F�};&�������n�ot��>����p�����WU��L��]2\�b<��c��|z�������A����qND�e79�YO�:��>8���W]a�x��^����"X�������}�����m$��3��;����HZ;�����w�O���������V��1��>��v�|*���i�C�\�J���uM]�z�|"l�=WD��2h3m��J	����}����w[~���@�[k:ef�@<���Lkc�����y���U@5UT*���V�����L�tIm��D;��zp�Qp�"��d�=�}<e��gi�>Q�%EV���6���I|s������f��f���R
fj��zM�M�O�r�j���vLm���F�v=�F�����������&���<����7v��9�$����I��e�����������3����>�2�$�p������v���r-�7%t���&j��w��6l�d�>$�Kh�Fw����Q��L��{�d���u��v����H"fu��������My[�{c�z�"|����e����w�`�s�Lm7�@I�G�����8R�����t5���u1yZU��{�Q-���	L�����_�����L���n�7��t�Gm$wFM�Lv�7�o����/��v�I�4!}�)5���~g�r�PH����*���_=lz��"��#�4I��n�;ms�����5I<�9���<Vd��quU����!�u���E�&��Q�/�!������>�6O�I$N6�K�Nv����������I���@c^�R��dMk&�-,������'N{�n������7L�U9�a��x8G,��b1�2����p�=lN�=9���I+�=��p���/&����0
V���E!D]�������S�T�t�_j�<�fa���;�Nue��$�02�����r�skvFf���<<Z�;��l�qM���OF�w.8x�Iw<���0��k����hn��SR��/MWbt����M����3�V�@'v.��z�/�b�tr����v�������|���i����)+�c�5T��Y}�U�Hk�}��9��7���U}��0����L\E/E�fV8��[Ya�A�p����wg��X<�B;���e[���_9u�X��_C�&�[��5�W���eXxQ��-��c�s$
qq���H��n����[�Wt���#��6l���W��[v::�E��%��)C�th;]3��T<�G����L�f�����|�K�/7F �f�n����d�DM�>���MguI%Q�{F:�����9\�O4w����^Q��%��U��I��lR���.1�X�� ���7d�]	��;�$��d��mS3m����*���o�|���o��Q�1tP�4m,��D<���u�@��VZ���q�kG{�N]��2F��&��4��g�g����_Kv#�������n��R�C�y��������I��$n���'mr��{l�	4I&�"A���B6N$�h��H�/���g����t�J�����z��y����}}�I�h�tg-��tgwFuD�uI���~���<��������/^&n�qD�U�x.�T)Y\�7�(�r^p��%��r�'t��4�5�D�i����v�wi��%�L�1�213�l[<�$�37gE�{�{�n�j������������������-&UQ%�d�����$�k%'��w3��T�����`����y^lWl�:���{�k�4.i{���;h�:�F����;td�i3������7�l����������S�W�0��s�tc����9�R����f���s�=�td�C/m$�C2^�L�T�v���$wi${�9�������;|RFo�1*�����2���d��Q��W���z�
�$ud�w�9B*����Iv���*�}���m��B�[�3�����]����S77�.n�!�{Y���
��v�t����J*r����9��wA@\�fM_���c����[�������A���\#`���������>��f�,�bMk���_���r�w����~�2�~�������&j��C��������m��&�#.���ScI�KrV �8b�@��XC�f�S���]'�=���y�8;�����Z��RlZ�����|�=kd�]�*�w��������+N{]i����G
���96i�>�uZA��m=6������q�r��l�V�=p�����mz���u�\Y�k0������4�Y�f�lz�fm"�|�n���(>�k�\���3��gR�iY�\?���\^T����+z�m�S5r�yF�L�im>w�����������WSf���/�T�
�j�h���x�Q�]��qO�U#vM=;���z��������}|+���yoG���9I��*�.VK������j.��'6�;�7h����D��T��{<U�������G���^�{C�;�"N�.)]�)�����<��9��f*�Z';Fe�I��gn���$I�$�u��k�1�n���,�����:Gp���(f�����'_c�������rwv�Nj�
;�L�3�d���@�6M�@��5�M�����C����k����]�cw�n��:�y{���M@w!5{�&��7d�4I����V�9�I����?��hSM��B���������D�P��V
{	���n��Ef�u�n���|]@6���d��dn��t$�t�.h�=�	�n^��37ny=Q//.�#�A��J��s��7����v���I��FES2���M�#�7d��wd�\�$)����%���+���`���3:Zz2�o��p����)N�{���g/��H��m&�'n�v��M����4�������7������N��Z�����_������o���j�&�����P��p������@�(���1�����U&4'
fCc7�!�i\q�����r�;�)��j\,.�v8x���j]Q^7VO�hL�����I'1m�|rfi`�L����&����y�^��k�N�q%�o\~�#jwW�4���3i����T��a�g{d�
���k���T��D.�6�+0bv
N�}���c���s|�c��X��K����C�9>��s��G������p�����������4"�g�t�����J�����N��4U�Z�.��R,0[�{�@v��z��U�m2�q������x;31��?�N'�?=�Q��1�~dl��=j��������bp��r]V{2wdT�<=��Iw�j�����y������~�|��m�E��}h�����X�������8������=�����o+x�Z��D���Y��@�B�Lt��a���������vW�=U��qI�;������|���1����Kfmw�:Y�f-D���)]W���O��uY�C6s�XfI�;�o*�WWb��x���w}m���~]�����G-�����is�j�����ASK[���R��T[#��6A\��+���3r�+	��/�$����0��N��]��Ex,�:�`���+�V���}��~�B`.�3�i't2�N�Fv�2m����z��{�����-���WsM�%/�4���8#t�gz��a'�9�O���Fi&c�2���n����I���<����
�����zv��WUF�2��������������mof�>��O��aFw�I��N����M�>�<���vg��r�w:����`�y;�@�xi&[�`����p�mw�t������d��$�Wv���FR��d�rl���+�1�2�����8k���^7�aA����*�2!pS����|����{����;�$Zw�i��}��d�\��������}��*��'vR��6������n��r�L���~�����������;�"����^�3��C3�i����y~�{��hyAw�<�.vt�\��N><������;��y���om3���N����+D��F.�<�����������Dv+]|��t����py��MP���9soRE��6Z����T�����d����L�Nj�.�$9V����#�8��������5z/�0��������
��R��QJ�O��5�Ktw�I����D��O�$� UY'
������4:p-���/��,���Ix^����[���d��?~�����wh���9�H�d��9�I��v�;�}�n��w�z0^���. �Iv���8�������y��T� u�7��U5])����Fw��Z��cqD/9����na�
�U�dA�fk]��h�'*g�����z$D�{75����z�Y�X�wku9#��t�Osi���,�S�cw����#� ��xxs�������xm��[.�f��^��#kEgC�D��
x�+2�2EZ�`�}��<��������yv{���������?��3������B_�A�>�<��I�����#�tA|��/�tp��]��N�bsII���������{����L��]St����c���>��x����@�F�5��e<�+lDY����N��C#:pN�I���K0|E�d3^�k>��r�R����J��7��q����p���I��f��];�@�Pub����]i�����B��%�-�u{,�g�)������y�|�4����[�R��3��	�>u�}�@i���[i7����g-�Z�$��T��9�A��7e���M*�np����`�7!��u[��$�/�I{��ki�h�Nn��G�3I��$�
����e���w�L���g���Go��>��r����Pv�������s�}���n�fwT��D��g.�I��f*���d��g����]��|������c#��0Ds|mz-5�0�*$5�7Uy���m����N%h���';�����wBE%UmS�%���]^v�uLz�c(�;�~��
��V�On@�s�Mj�{�}����I��@:S'n�"�&-�kEZd�����y�/��w����N �K��+���JN�n:���[�~������yy����W����I���6����h����3����>���>�Rf����6,�EuD�%t��TS�&q��y���LY�Y\�1��}�5d�&����toh��2�L��f)%W��s�_���UIz��[���_[9�7���gv����KI�_:MR�N��;�%��3wi'Z2u�H�]�@�:Z�Y,�+������(�J�ZK�<�OhB?U<�omk1��{��yu�����&�L�T�m7h�I"���]���d�sD��/:���r:�Grh�9����}������k�U�'B��z&T�&]��]@�vO�l�-��9v�R��[i����y�Q0�������;:�^�����w�1�Ri/Ei������|�����UL\��{L�`��>t4��>=w�-��2U;n����_��a��>�)�@����!�M�w*�y������#]��o�]��w"�Y���{q�����i�����^��U_PB��|T�N����[e;$;,��H{�<��!eD�\mY��dY8z��w=����������2>G�(I~��/��X��S6�<:������).�<<�u1��D�CoX�]��a��sKw:%Q��+}�8����2M���`���A[��a��!���8��+���bf9=�__[���F8�8rO���n����-�sz����p�����
�^2P>k��r"��;IN���x&s�u��y?n�_)�q-�;c��2f^���Q��#�|���n*{����AV���P!��=��Jc������h��\(�Hm��K�����21�igU!�j�fs�I����S%��.�%�{��� U�'�&��6���l<s���}��:��^{��n���7�6�������o�4��>��|]UW�9*���&v��/T���m2kl�9n]��'���%�r��<n�oL(�}�i�''f1�����\;����|&r�;�L�m2oBa��h�tL�m26�3�u���W�������0G����BO4�OI9��7�}�&����
~kd�$��7����,�h�V������$����~��{&g2b#���=��n��xfj�D�r� �b;�f�Uvt��,�jm�R�Z$�I;T��d�@Y��F��}����N�;\-c�.Y��w8d��J����M�|�$��i��RsT�oh�n�^�3��@�6H���G6������0�������n�6�m�m����:I}���fU�^�2���Fe��z��MY>4I>���������m�e���7ga*������[F��S�;@�o�s���~d��AI��I�N���i��oT��$w�����E�#J��u����jt���u�b��o-��J%j7�r?|0�?Ue�'��m��T�{�2�w��E|��'�1�M���K��C����D���v��:H4���/u��=c't2w�I��tg6�9�6I5VHk)�������K&"�GhO�g�G�E��}r�w������r�^hZWM�!{_���7X�����*fw��4���u7c�2s�2�>��y �����{r��$�o<-|��`����yN���8�mGl�s��h]	
rC34�DY����
�~�=�yf�E9Q�(���x{�&&��=�t/�Lp�5V��:�����Q���jg�����������"�W�V���6����l <=q���&p�c�d��nr����kv!��r'0>Qv�a��X�y_��u���k"�EE�5�J�v/�&NK��S���W���:9���|���Wxl����LP�iW����������YX�=b�Eu�\:�!=/��"8.i�tW�>>=����P�}��W*�n�0,�����)�P�K��/A�=Y@�DN���XVA6i���B�����K�w��R���0/��ADvm6����5e�R;��c�N���d����W,�wB�������q����DO;�s��v����y�����{7��om0	'H)$�h�6I>TI���@�Z��S�X�J��ZJ����qA*��@+���\�I0��Q����l�=&�"h�>�������d�FwBO�����������k;��������>�o=P��4���*H^�*�7L^Jgd�=z�M�/T�����7i D�'�p>��y�Km���WPh*�wM.���[�t(2�������N�I�:7m$��gz�����Q�������j-�,}���E�X�$��V���Y]���������y�>���8�d�����d�	"�z���D�H�&�>5d������A����7&�3�j�����hu��)~�;����{��;���I��t&[D�m$��'38Rm��������O���{y�H`qw��\Z�M���������6"��HF�;��rI D��h�/v�#m$�T��f;��l�y�%�_d���S����e ����;��8T������@��g����{������I��D�6���d��}7VHM���t�K�M�m������J6}���=W������-�/��+�m����'*�w����{�gzv�'m5D��3��{���w^��u�,�u��QJ�T�b����q�����u���������E�(y�b]w�ejE��/�U���:M6��8D�����"�������0�M0ty	���}�[��8k1B{����J����uN�X��J�~�k�I�X�2�����
�=��m��w������o����jvWRy�xxh[�s'k�J�1x4����hC/1p��x.G+�.D����|���v�Gic��R��`�o+���[f���tl���F���m��,w�^���7���z���6���������47/��j�	[�tv���������&���OoZk�V��B����f�G�������F��6���AwlS������Or`��l	�]���L'��9E�({N-�NVm��c�xu��r�����s|�FKI�}Yh��E�<���l�Y�H�����-����J�wH7�^����f��a��yai�O�e������e��wFe�&s�e�d������)���%�����;}�!m�*3,�����>���9K��N��%T����K��ZImn�1�gwi I'��Tm;�c
I������Y�]*>�H#=*�����vd;a�d7���?�;��S&��s�fr�
$��g;D����v~�w{:o>.�y��1U���a�B$�.ws�Tb���m��{y1�C���7�����Bj�M��#tHh����h�p����������:����	��]�����]4��m!�w�bx�j�u���y}�����'Z2s�dj�m	6����N����������O>������������f8cz1�f,]�^7�����%4�3F�)���~����F��g6���9�I�i��������=����5s��m�T��+{����w*U��HMN������[���6�N4d�v�h�r�_9%P��@]nq>���]�*�:oN�����
t��|�����iW*��d�s�����I��ZfmS3��Lm������������_�����~�{�	��������7���.�"�b��QbE3���"����*���m[�(2p������n�G��d�K�	��)"�G��lX�K7�e�����4�d]����������f[i8�L�L����H$�@��$z��!	Wa��.�cP����Y��+2 ��/���1U��������*�H#��d"I9Z=��iQw�g|)Q��LMsD�=:oD�:D������e;9({�wJ��c�>lt|&����g��|*�C����$���^�|������Dz^�7����������F�+����7�uVK[��KSkr1�U�������[q��b~��o��UL�6����;���t=��Hq;�F�@)���zz�}����D��-,8hGn����{U�@�*z�
P���p�	fM7�����U���hQ�{�O�9qi�K$yX�
b���N��{���MmN�|VX5;�),r?<���~k�_-J�+�����V��0�R�Osp����Y��e�^7.��IB��H�I���w��$��
s�e�.�[��B�g!��u'b����ci����:�N���Xt�90�2�b��}�-a=��
u+r��QUDmk���4��;NK�7T��������<�����Gm$��j�������h��Nvs�����|}e���Y���4���Y{����9s�<r���a��W���I*�RU��]��Z$���wv���;�L�}
+��,���r���w��j�����fPX\5��(X,a���fI�
�sh����3v�:�I���&�Qa��t�������-�����m�$V���w��i)U�7��r�������wi�&L��IJL��l� A�H���"sYn�c�5�<��-������/���u���o8��9Fajq�2K�h�J�_$�����UHI�L�v�ki����#�����}���0�Q�7L�O{�����~'�,r�&-�>��i�$�$�$�]U'T�v���m����������|�K�G�2^urU��Ggtz��������=W�
�>!��������7����I�����w�I��L�h�����n���tRx��k5<86�f��&I��.=R��RX���"��oD���6���'.�-��^�r���
�I�c���}sBLr���6���YcY�*(�C���v�eD�O?�=�����&Nn�-�3�F]���D����i�|���w���?���>�uu}��%&�$����M\%cF�3�������_j�������|f�":r�%��h3�i0�N�|}�z�^nz�1�,���a����W���Dc���Sb
��{�������}.��dP����Uj�p?����7
'���b���]����WG�\IR\r�@F��5�kb:������~ff������/
�V'���}�[�d1Z���`��{e[u,��l����"j�3���!�R��8O3W{�������/5��#�SP�;�2�F�(����{�+�.n�q���6��<b��Pz�f�Ij��d5�R�=`y�
��s�f��38�z��}���KE�����&+�j1��V89��%����_���A���l�m����nS������#��oF��*��~�����V��3#
��*����-��0=�.zr�S=3�M�:E;�k/.������M]��k�sL���\��Y*�43������K������MT5q�%��&>�����^��O
��WHl!���h��:B������L���!�S��b�6�h=�T34��/4��������H�L��I9h�D�;(�:�NQE]g�'H[���p�T#��2�g���5z;��[���i�m&�-��v��1td����	IU�������[9��l��%�W�;>�&�r��&we3wh4��
{4���m"�K��n��wi':����g6�$��������}�O�u�������#���z5�{e��a9=a)]�A����j�Z~f�n��7�I�&s��wC#�2�&���+��A�E���b���Q%�~v)���n�����h�S��6�������;m2m������
U���/U2k�,���]qs;���.���=,�����s�4_������t&[i'uI���{D�Y>���e�����w'�b3�g�\���z��t����{;Q�np��:�y2��j�h3�2om'{i�{��4I��$
��|]��0�/2��-����9���,,��#�nM�����]����D����v�6�&^��rI �&"����d������m���x�����'�R����J?^����--��q�5������Y$����~�;L�v�wtfGh��3��I��?S>1;����&� �tV]��R��^[2l�}T/g�=v�8���|r�P[��wtL��e�IV�-�I6O�{�����~u�*d�#���n����x����*Q�VS��hc�/����>>'Z�}���#+��(��o�Z�:�����A@�
���w��DR"[���"��D$}��=s�����@���]�crB���=���`@��^�������u�����#�$
���W��#i��}������+����>��S`���*b�y��5r
���/ma]����}�h��R��b���,f>��Ja�s�W�����("B������n�
H�{�1����OM����8�v���Lg3"�����]�����V��E)X�3����<�������<�����D��x]����U"�,G�}�x7G^�^�L)����#Wu�|���Ac���A~h�~�t���fm��������������]�U��l����g��Qoxa�����g��������$�����}�=�f�EA6������ �N���c{<�E @�'�wx�z���W
��D*�����k���$H�A-�������jR(5����u�!O�������C����.��]c�(�Z�F�+�}=�FoJu�}�J������n�;���P�bs����Z`EW�n��&=��`���4(N�e�I����x�>�Z��s������!N�B ���kk=��3�#���>���W"�5��5����R�(Mc���BJHD����u�g��l��D���}5�1��z��$"9y��zYr���H?	$5:1\����<������6+���PX�U�8c2r�����������X*�Q�����o���o���R�o��A%��U5`�@g����+���.�mmcK���J�/s��y�6�$(�^~����Nj� �H�r����������E���Z5�Nn���HM���w�
"T�Oynot��
���w�����"G�#�Z2�hj�v{2�Y����:������4�o��nq�P���3����Sik��-\�*7
�>��I��A�s�Q~�\���C��8u7��:{�EZ���������:��o�;���RZ�{�;�P ��T{�&7��.AEE�|���c����r�$"�H���w��_�������}�����B*)H�}���D,�"*��:�v�����\L9�����$e��fr���{UzYL��+~o�
�{u��6�S�R/k�K����M�J����k��� ���t9�Uc�g_�C������z��`�I�
(������7|�QHE"������8���
E���}y���V�� �}����KH�$L����_}��9��*�9s��@�{{�q���C�o)���g��r��oM��	�j��B�w��QodI�o�����#o�G�{BS������S�Q�=��`�GN\��/Y��t���������6����cu�p)Q$�r�|����(�H��]�sb$"�w;�.�D�3�q��DU�;�;��H�^P���9�rjm�DDQ!"�w�g����3�%!�����=���~�������8��@9@����������������8}Y]�r9�tlz������������63���J����g��*����]��-���������s��1���)QU�c���&6�BH$
����{��U{L�}�z������P������jc��� ����hB! H����b�X$Qn�������E�PS���.R�(������%�j�\��hw����A��O�`,M����;��<)���[�����~���2�"-~�<�����2����OW�����k	������@�1z�5����B�$U#~��.�a L{��2�����N��{��h�P�;�y��u��q��y����cp��B�|�7��D��"�U<����3h���U7"��/0��s+���Rv��,�U)����W�&�����]M��MOl��j9�)wF�����n�9��e��|<�j[��5S��6�����w�r�*�����������}�)HC~���x�;}�)A��g�b���"�ng\��x�Z��7����yx���Bz�4�$�MFX��"#B]��za���(����_\���~���x�]��x3^���Y�9�Vg�<*z��r��|���~������s<X������,�	��K�Q��&��{��*.(����A�	��b�\���$ ��u�j����}s�������)H+��/k�����$S_�}��;EB���]�7�B@����=��g\�P�QM{�>��"$RFW%�n��)�,o98�"^�Y�2eP��g�4�(���>�1([Y������6�>��,����o�i�G�����������FEP����y��*D�L_���9z�����u�{����]~��C�E�W���~��NDJwW1���"��"w:��|��QH!E�|�D�CuH{��7�&P�mv�~�}��L��Z��R�
���[NL9�����������s���B)
HB*T�Z�eTP��\�n�%���mS�C H'��DR�) VW=3�wZ����jo��\�9|�4�w��gY)B �
��/�����>�:�w�K�]����" L�3���3������W9PI}�
�P�HH�g6��c��b��j��c��u�o��2�����
j`�]�nX����$nqY��Q"��(&�w�}y��r���o��d��{!�(
B�$Ro��2���]��|�����qh�	K�E�y���Ea����\����tN�=6��:i�7t����������r�C�w<I��?�gj^��i[��{�A2z�M��;����Lg��#;��>���*@�F]�9��n(H��7i����P������+ �"(~���>�;�HB����^s5rEE���w��� A�H��2�������"!DTAA B��S�cs+%d����|s�"�@ �@W��
l���U�N)�M���moX��5|A��
�Qd������3�51�����������)J
T$""A������%�[���&������"}PU
(��)}��xz^no=��3�p�f��QZ�[W�vgx�1�k:�����93~���
?�$�#s��KM}p���'��1���x�{b�20E6#�=���o&��N��A��(��D�����c���,�5g9=y��:��x��oZ�P� �B���J8�m�5zdR��r����R��|�~��`�Z�V(���������G�����"��D�wM{*��	At�=y�8o��{���m���K�x���s:�J@��W�2����.�@+����fg��8J@�g��f>�3"*"�_w�9;���D��������c�� �")������A`��sx##"��`���.v�1�5��{�B��T)SiM8i�D��b�K��lUki�A��c\��H�T$B�����gW�}����������v�2�$$�G��~?�B��
����������]��k����E!PTH����=���Q ���z.�((@�R5��'q�55�c�����:���^�w��W�����oy�H
JBI �����V�u�m�L�v�qy�����{�w�b��%)	
&��N�������ns|�ox@]*b��("1X�<w�������m�r_������9���
�9/
1����ta�OJ�l��I�H�)���Y�8_&����[�39�g���;��.�	�����{����:V��u��
�Y��)[�=�."�x�9\e
����r���=�gz���@�R�rgx����B
=����w���]�	��sMo���	�}w�{�hH"D�{�c���AE�������L�PB�s3�~�{�D����1�a�z�>y��DDP��"�B�_�����x���Q^���or���ID8� �I$���u^_9�S�7vO\i������sQD$D@Ix�{����*N�c�����l����$���I$A������M��*e�k9VVX�7w�o�����B�A"�EHe&�|�X{U.�o*�	q'��"��s�}<�nh���|�1���1����=����BTJ�GU��A6(��|�-�ek��J��REbETS�ns���y�2�kx���g�������{=�"	�  I"�
���
�Z��Z��K�����y�>��[�Q�����R9dM�(n=pp�����������:�h��GU2��_��u�lV�!��������P�d��t_`�X�3:�Udvv����X�����C�5�iVk�l�jW�_������\�@?~ ���������E$R������	$���^�d���G�u���<��B$*����FX�3����H'��c��"���[�X������B��f�c��LE���!�	�i��9P��/�.,�f*�(U��{A� �DE�����79�����>���u�I��:��|��@!��I���+�������;����kt��B��Y�J�q���*���4us{�[BP���R$B���������fg���k��������)	�������|;�T��*�@������TJ%(��g�g�����8��}M�p�t���*DE��UB~}���R�M�{2��k�(_
���(�DH
DQ
g���X���h���1�������P��'�~�{c��k��A+2<N�kho���e=j��G��<��=�
���(Z������b��Ne�����yn�z�xE�sz��4���nxOu����o�e�Y��l�� �~��J���	!IM����������!)[��\��1 ��E0��6����5��D�J"}5?b���3q�%#g�N���@�o.g��&����)\�1�Q4�
s�u���*	1���:���u���r����Mr��kno��)
"$R���v��S�\�)��D��\wJ�5^�E"�D��{�w��9��W&��tT�w�v33��UY�6I��B
����s����m���m������ �*"��E�oz����F������x)�����"��!H�D!���b�8���Z������GwO�
��9�su���P"R!)Wk�_�5uUP`�vm��L����H�0���"";�L�w�Z���^��z�be�����>����)@|)g��-K�����0�z>��'�x���"�,`�|�oN�����!���u(��>��M��=~W�����������y�W��@�g�/��3���JD%�z�g�f�V�>�;�8��&Z�n�e\�W���o\��,e+�N9^��&��n~�v�.�(*��q� �" ��~�;|�������D�1����w��Z�B"zs3oC) �3�k��'��������QD����;��������KND�"���� |�9����!_n�m�
��K�����DR" ������u�_q�k��;������� @D�E(P�>[E������k��V��e)�	�wx�����g�|�|�>����'�o���1FDDE��M�K	�����E��K�%�C9'i
"EL����������sW��|�Y�kSy����]R@)������~���r���v�������ssU3H 
(����\&p�fM��1���z�EGl��JB�DT"����=7�=mM�(d��2��y`����A��Q)$�k��������k������L
������%3��{�Q��w~�����u��F;���QS��n|��2��9�n��b%^i��ZUw�U����{a �������Ui�������_&�;���R����{y@(TA���s����_���("��w>�=D�c%�3�7�[y���M�"(R#���}�bq!�<���� *7���\��� )L5������f�w-(�}��>��=�Zo&g3�������E"�D����Ok�c����k|������}��;��{oM���*"	!��X��S|�����gU8W��!��C8�}LN��_u����1��zw���qQ(��E*0������5G����e-g/�j�@��P�$H@�}	�{ss�k
tv�l��:Q���;n�@P�
R)���Nc����f�w�g�7������&�����%)
�>;Z��J�����lG�9��,�<��*�D!(����zcv�{��X�{8�y�o�����{���R�")
R�I<�7�c3��]���D��������V*G���8�q{&z'�UpkP��z<)��F��^�V��U�K�����EG��U�u���gW>Y�;1^hA����ZV����6����,C��E)
���X�{�|��b�HBB(�)��s��c��B�)HH�BQ��y������B�B�!P�)�����xQ"����{������w7�T��I	�3������"
��A�}�f�N �R���}���i�]��.�|���(������7�<m�;T���'����V�u��h�Grk+y^��L�p�J�87],%����l]A���g��[��qF)u�����4$��r�����������.����'2��#�����a�p�M����'o^)�h��8")��.�����V��
����rS
���7�<V������G;�o5*��I�-��I%X����z����%�Z�����	��80���,+����{*��PS�X��v�"�D�aM;l>�,#���:�Y�.��SrX�)��m��[�����$Pl�)�3����L��euZ��=�l�����qy�H��eh�_a����nC��ws{������1����}T�������U���@��V��P��\f��H���������2�m,��
%a��
��N�gj�e�S[�-�]p�
J�j+�2�jv��6Z�W��kNu�8��4%[6�Fh&�*��I�wr�R+���C]WQ�u�f@�&�J������c�������mh*�U
���(EbS��������i��!�[�l�a=
H�^���M$��P�V�&^���i6��N�fG���3z>�W�K�A��;6�t1���[2%O����8i�<��K ����������N�h�
3��cnud�xY��.����_v�p����hjy�7���YVPUG�e��u.���3W�0�e�g��g>��Zj����&i5y�j�L��-��E���������o��N�`���N9������x�J��������K���e��U��E��K�K�>�	�>�wgq�����D3]Yz��'l���o����r�����������(T,�9�2��I����C�Pt&�^������q���+:����P�jVv,�e
z�om���7u�]Y<%�b��Q��G3m�m���;�Wx���f�Q��`K1��]��vT8��q���m9�Aa��.M��i��oU�;��^�p��CF*�@�����z��l8K,l�x3Tv������+��M���]���2���B�'����.2^��[��lt|���	�Y6�!q��D��
������D�aGU�/���hI������U1�|(��!~����V���~��q�e�%��h�U�B��k!�}%�����&���-!��G�7|^
]W}�WYi��e)�
#u�;��H�~�D��=�7��;)�<�����������>7i�eoLW�Q�,�V����[hfW]J�����u������j�
)��a0�6v^Q�.�gv���n���Z��NS���"�6���A�k3Vk�F�_t�+�U'5�:sh%E]	C��a�1Y���gVY����}Zw�}s/w/��0\����P3fA���[D����	�D��j����f���K�	[�F���i6��������������uj���2,���z��[\j��f�)r�t�0�'c�M�]�UI{����Vk�����tf�3/��zY�n��R�Y]�5������+���m.{�l�h��x��':�
`��r#�N�5eXVb[`'N�C\�,���.qjl'����N��S��KjV��o�����1gg,���M�u�$�R9;(�E�����R���K`��)������+x�HR��U�����q��n��#������Y������J�n%vM��'��K�Qc�:�Ca0��T���	��v�f�p��Ul������u�S+�%���eu7�4a|���.DG��J�ts�5iq0X�):��e<��R$�����<�����}����KxucO������/��`���6k��G�Q�4��u���}@��������9_B�
�
�W���M�)t���"�n^f�����Z�RF������-�os���-Q�%�DV���4�X�G31;q�\�4����qg:�F���������a]v�Mu����e���[��+�����K52���
L	�d����R�E��i���6���.V�����M �����_>$���]��1���UW������M�
��s;"��������V�k>�6���H�����Bf�5��BU���XcI����r�>���f�(z:S<|�B+L*��o1{+_����	�=�#��51p�0\Ggj��34_��
Q�o)v��y���������������>��x0l��Q1��_�S��f�Mc����w�*���j#��1	��,R{��H�����i~������_d��������F�i�T��
V�$g#5���o��.�}Q��RD�l��tMG�i{����6n��������qPV��.�:�u�h<�P���#a�z5���h��G"�#�.�aud���,�,�sIno�g-�k����m�F��\)�����S�2��
v/$�k+R���B���.����W��W��i�['.R��3�\��6���-�B��*=��qk�6I�����$�wi3�����-�Nn���L�y�)n�3����2���3ObM�9��N{<B���Kt�z�t[9�}�yQ�%��en�d�FHCF	$���G=��M���������F��v����p$����>�g,�^56�3�i���;���=7vH�Y�EZ���BUf,������v��t)��9mK�-z���z���q����N���T��j���Z�q����38S'����������:����vCR���6���9/;�%wo[5Y������I$m����I?nO�Rn�7�1�;��/������~����,X�+�m���u{�k#TM����*���rfF#�n��;�ud��tf�%�L�i#h���zM@���h�����a^�.������c�La
��v�r�g6��b��`��d�M�&�&���d����$�$�g:om$��'Jse���{�v����U�x���+m���Fu�{i�W9j�[:�L�~��
�M}�t��{I��49�d��';i3wG���s�^��}��{�F���g#Uko(����U�8�'��qp��c�����qD��uH���SFNv�>�$�"��5�V
�y#.�hIT��T����e�K���>�3.��_/��}�=��a�8�I��N��;�r�otI7T�������~��?����j6����qqj�GRQ~�p�g������^$���E��P�'��Z�[
O��w1�p���|���=���s���x�R��'j�$����-�U���wGJ�����u��J�j�tL�=�hN}<L�8�#>����y{����Ip�%p����&5�B:�b��q	�!����f
�6����$��_���3/��<�.O�$��9���z�C/�1Q&>�s{X{;����21��'����
�a�S���a1�F�dX%���e]�'vN���N:f�{3Ll���-8�e�����u���7��\v�[QmLP���nf������j�^�����w|��m���'$Z���F���	Y�v/�����]j3D2�3�'r�Hx�+�J����o=Y�SI�8�\lo@�j�5]�Nyx�|��=FX@��%u�E���T/�y�w|�]i�V�[�[�S��n��?3���������R]�f]Rn��=5VHj��+��ML�v$�P�Pt��+so��?j���T\����;���wi�ZN�N�1�=���}4l��=����9��]�\b�����3�
v9�+��Vzv��U�
�����������h��I��L�hfm�';�]���=����-������@�3�oN3��`�j`C��A^��������s�s��-S"��T�v����:�L��+i1��y����u�o�k(c7��Qu�3���!�.;hm�C�\3T�����6���W�R�T7T���8S%�e��-���3��=3l�{�9'k�M�h�k;��	����A��u��d�@G*���U��N�L�����A�%���>>l�Ioo�6:3Q����,�����c+fw8�{>��������$�wbK�L-2�i��$�i�{G�vSS.� �}�t%��Q'u����/f"�oLS��&o��h��TI#�����P���e��:�F���
HF�s���9Y���������U>���h^d/.u-�*]W�@��$��`����Ff�L�����'��$��%8�o{�q���-u��K���p�N��@�u�	���|����b��%)��2w��^�V�3�L���W�����?��p��|#`���3��%�6�O-�	�5��q�t��O�G7o��EO}��vy�Z�1����z�4��~�VnzgF'�3{q���=��f�mX������uf��B�}El�����z����g���:l���W��V��5`mDMI���x]��7��=�c	l:��m�
���� Fj����	����8���F2y��6#����$NG���|�X
��=���f���{���s�0Y������+v���dE��
yR�
��n�zSc��T=Y�(+�;[�������������r�GrhfU��3��N���sM����������.�x�`}����9�����d���Lv,\���n�U�6���Fp�[���w'*7L�����m#3;7(��a��Q5����(�t6����<�Edv
:v�p����������HO�=
e���=�T�cF��>�y�}u�~_�����~��{��^�N��&�fr��ZJ��{D��L6��}���{�v�6���V��cv�Q�T��r����&�o\��Jx��$��Ud�����3��9�3+D�h����iz���=<��/T����:��<�^�����C�%��2�?_���M$��':h3n���d�L�Ha�5k�j�}�L���yn�k�r&����pM+�4
�;�bVM�
R��:��l��n����'�tI�RN1�2���2w������z�'�� �!
��"�uwf��:\|�o
3�N��Eio�%W������'j�';F]�'Jfsv�� u�r�Fle�ec���F��*&�J��V8Y��E^Ld8.�T� ��8�1T��tNP�v�L�L��$zl�#�3�����N.����)<�3�{���~8c�D���W<��mO~���2�������������0��MY$z���{]\P1v�J�N5�Y�3Q���qx�p=�(:>�4���/���5��7�I����i$�v�EY'�,� �Y �$�Yx[�K*�2s���{�������,W����<&s�8^Kp��1��p'O�4l���$�����S3{�oT���2wv�;�I��{}8��IG�������f�on�A<>�7V�(���r�Cw���y'Tom2v�N�����uU_����rU|�U�����#��K�Ud[�q�eA�3m���3H��d������e#�����N���g��c���Oe������8sVIn�v�(F����t�7�7#���K�����,�o�v�B�z/
O������F�ob�/z�mF�����,jbv��yo�x3��X�a�M��<)m�oit>��`x{o�R�rX�}���L���<��m�����<<�G����V�j��pc����2�:���=�I_X��]��K9�C�%��
��F���D����Y����I]��5� v}�
�;�W���%=��pmQ5�)eT�o��F�8�������#�R�Y���[�d�Ws�������XXlla�;x����:��8
���t�QYU�29�T�q�
/��B����U�~pq�y��	t}KA�n����J�������yO�U;;�a1��^h����UY;F+�[��|��\�}�(�s{;�������[�,cP>�d�we�1�J�+9^��fm�����w�d��&Nt$�n�Vi5�t����_Y�M|�9�5JW4s��H�����{�:��gX~������o������������7�d�v��B���e�2N�FN�I�������rY�O[D������h���M��)^���H�7�/\�l�K�*����ki&��L�m3��v�����y,�3Nxop�F:Ut��
��%7C��������R`���1t����Nm;BM��^���IuI��$&���U'S�Q��{����\�j�Y�3��.2�i�����3�g�T��$��N�'A3�n�>ud�tI>gs���LZ\���������*�M�e��3��Is���cO��������{���JS3{�6�&]�fr����K�-�7v��������Kg�v~������mP����-�����\Vi�X����"n��I���F6�N��9v��8	?���������Q��U�nu����w����< ���e)*/F����O�wI5���MZfm�KBN-3*�#����Nyc�E�������x���h�Y	]����A�_3������YY��y'7D��9�Iz��-���}����G����jm>����xR=Mv���]��`���G�]s�Y��UK��;��#�TrUP�JN��%�3A�m$hI[I�7���;=�j=U�j��7�����4��x
Y�G�Bz��5�������J��K�������y:��i(�yg��DW�f�PK�����a�������/���~����� ���Ye�R�-���"qJc�UtfA�}�Z������=��T��O��~���R�1�&���%EM�]y-�+��5a�Q��G�=����=��T��Tjj�O��{�`���B�o{�{���f�����z����lC�<����o\�"�����&7��R_1���I���}�����-��\
p�
�j�N�����z�~.XOz����$�f=��J��Eg��;����-��C��"lx�UM�J
���z��I��q��r!75l+4�.�/�q��}u"���q\���z�����H��4T�����4�CQ�{{�x>\:/k�V����Mo�C�0�.+�����%w����*OVY�%��!��	HvY���d���3�'��s������6�g{������0�BNn�>O���������=;1e���X<��!���J��C+��]��j$����S�������e�f�$�����';�������Mge�.��������
� ���u��w�(i$��u�������>��;�$����i�U��h�R]RN�1h������m������K�|�}�B_�w#X��,nj�2�[M�������|��}�6�m{�u�������D�������{�=������w�u[;A�<�����q`�{B�A�R'(�fa����C�d���d�3D���'TK�f[��2���}���}T��[�����8��u�����P:X���d�s1J�T��f��EU��rl���mS;�r���L�m2���>�A&�ZL#�W���'�.���,����o���`��}~���9*���L����hI-��N�L��O�:y=V5����+mq�\���8&����/W�lQ�F����N�w��otf^���[v�h�����@r�T��XUn������k$k�E1K]e���I�'��D�:�s������qi�h�m���i�4I�VHWd�t��y���@�������<����g�we'����L'����o���b��=&��&�f6�5�f�im��RGw�?�Tm�S���k$�J�`J/8?x�z3Wp��q���kk� /��/�5�}���en�.��Z�9�r�*����R�IB�P�>����"t[��ox������({)H����p�B	!�����lr������G�xJw{��=W�KK����LQv�9���[��7�a�^R�y�S���VGZ���=�	��b9�_M�o��
�N������[YXc����^�Yl���y{��m�u�ST8n�R���#{��N��C��\������W5j����m^d��v;x��X�-��r,1�;���^[y����g��/\��@�����n�`��N�V=/�r9jQI����!����]�����������d��ig��9�����s�3����y�{������Z4C"����4����������Z7�:��7�h�v�Z��im�Z��U ��^P�Q�VP�
gv���`��X���V��������������{����Km%Zg;Fa����N]�m	�)&�L�7���=�7�/<�����v-p����5<A�{1����v_S�,����������t�v������9�Lj�w��cEB����"���v�z�����3�[`4}��
������G3I��I��wi��4�Nn�r���=���z7TPWdn�u�GR�Ew��a�-������u����C��U}"�7T���9�dm���I}�6I�.��83u�E��jsU��K�M�0]��*u%��~G���:�����tI�5I9�`I���r�_h�w�v	�Q����^����q�����&Lb�^(��������3�6���m����E]�}&� 
��@G��3W+2������|�olrC}�^�W�����%���w������������k{v��N��7m3�2=����~�O���(��.�����d�]�\�[
��y�������y�N�3;�J���7h��3:�2��O�~������zs�3M4�u^����1{�	��V�)�v�X�o:������}X��l�
�Y#�ghw��#������ZIT��������!��`�v����u�������a� ��v9��?*�7t����3�HK��L�3�����?��f�0xN^f�^c��������������}�)Bs�>iP�������e��%���X�6�!�n{����,�!Z/�����C��������ssM���q���'@&"����7x��x�����6�_"���]��2��
G��xy�K�{�a��'�exze�[S��t����H�zf�Q'2"P��-]������+����J�B��X����6���9�n�h�@{�H,��6ur�p|�����hM����y�4(7�����qU�,�����Z�fu�F>�-����8��12�f��Q����c��_i�Cn���l\����w8�j�P����n�H����J��.d�2�A3q�����o2M�CTk1!�L^������J��U����������&Gk�^0V�8�e)T,�!��_HFM�ml��dj��(F��Z���������ka>�X�\��
���w�R�h\�T+b�.�"�����j�g�$�x`�����
��zN���;�t���{����a��h������9���}h]L;������K������$t����ZS�8[@r��#����>o3�s�x^a���7{��:C9���H^����!���a���`�
�9�����������uML0�r��a��o�^�^�Cu:kjc7�"�������5y����2�0,�{�7����$�x`����<�8w��`�9y�����b��*sm�x�[�x_���t�V�3�\y�_���Fi���B8U�k�����|#������������`<	9�0xe�f���`���������d��0xV��UW��.��ij��G��x���]��N%���
F"M��<f������vNg�C����!���N���{��
��z!�g0��Nfg�xUff{��9�<^}�'�~�c��tu.��K�X�k�HNf�"�*�U\����Vr�����W�sg�xUa����l��`�6s3���wg0:B�9��o;��;��N���������6�����gO����2����y�Y��95H1�X�S����=����C���a���{�����!��0`��'0`�����a�#��Y����%*U�/7<5�vP��$�����B����@��pf&TY}�6��{����'H]��C�3{����9���6���t�{�w��9��{��{��Hg8s��I�����,�qX��9`F�3]Enl��D��G���w�
[����3����I9���9��9��<,�����N`��9�<	��=��
��rU_����.r&���?����B�]n��4��t ��?\]�T����8s3�Vf`<.���;�:C;{���7o;��
�{���7y���3�x5�}�?���3r�c���s�"�)(/������	�mE��"��.$v��9�3L�}��E��i����N�*S[�����*�.�8	!s���M�n���4f���u)�����Y���/m��]U�������=��]��$������)��5�'��W���zpw������2Y�yO�.��`��c��������k0h|g�v�+�!���gs��I���e;�j�����}H�k��[���\<�s�Nv���>��_��7����!^9U)��Y��k��f�TQ����������U�����&�`9|gS��F���k���{�6����:�|����v��w<��������	��KoN�s�@m�%vT����1������HC�#R�@�7���}����,�"����Op���,{��+I2��3����� ��������7F]Z�F)`'�s�,��]�H����������W�\i����0;� ~�f��2�`��Wg3�C���gHm�{�t���zN����N����Y��<���=�d��E������p���L�E���	^n-�g���e8|����4s0`��s0{�����7y����l�����33���s<0xNa���8s3�<�G�|Y�N�zs��u����s?^������c��f�P�e�^�s����3�<%�|��D���}Gu%���QI}J��(�{����<$�fx�|�US����b�
%���9��s�$�q~B�����P�����Z:ox�����K�_V�I*���R��f��}H�����t�J������}I��*��)*�W��+r�>�|iLD��m:����7��Xv�*L�%������=�.����O{���s���{:C{���t�6�����{�t�m�{:B�s��H^w����{���9������
~�g�zn�U�����.q�,�������f��x����gO.[7z��(�g�
�9��ffg�	�9����N`��d��`������r�`<.����>5���y"�y���e��v�������BtfF�GXBj ���S����>d������`�$�g�
�9�<+0�{�����<	��<-�*������������4)����X����j�h<���m�n�[
����j�mwg�p�`�5y�����`<0��*��J�U�%%_U�R�_V�R��N�o����?��� }pE������S}�����r=�.y���x:3I1�����M��`�$��0x9��!���C�;{��!���zB�9���7,�{�<*�3=�1�	J�dS��j;G���)���Y�e�_k0d��z
��gudm�LS��W����}E$�*�������;��:C;y��H[���H[�����xN`<.�3�xw����3'�%m��y}�Rs��r����j�����w6�����|��y's�w��"�/���(�@�0>�y���au~j�j��o�
XQ�=#pb��T
+[��
u[=�S�N%y��w\V��w�3J����[G3�snj��	�l�(J_9c��z.���������u�+&s&��V�����"v l,�m{�z��g_'�u-�<</m��/��h��`�~�����LuP���
��,+����f������-C����RJvC>����<{���y��$E8�^;}beq�-!��-�"��}M����$bU����K�K��j-��jL��OK��'���}��|1YY��^���r��f�=�5L���=�
�5��KW���vU������$�e,R����YL�w/9�;M�}GV���D�;V��'e��Tw�6���H�p+��v&K�0��^����H�|�]�8R!���]o�C~��hh��>�?
�9��^�0xM��s���:Cw��gHw��{��y���33=���30�sUH�3>;k���[���uI���*�d�=���������K��������,|�y�`��N�D���
���0xy�=����g�xY��<333<0xM}9�����1���j��d�=��p����]����0%"2�#Q��3��48�����s���/y��:C���a���{'Hw�������������/;�:@�s<0x|��+�S��K���c����#�}M~�>���I��|qa����z�e��^���������4N`<&�3<0xU�;��.���t��;����;���-�0{�Q��{�@���}����p\��F����k��-�F����*�	�	+s�� {��XNg�x]����G30���������t���{:B����H(����uy�	���}�98���(s�������0�����<G�Q����
nJE�����9������D�{�<'	�<2��`��y��0xs3�<+f`��g3����w1>��?^�Dc�����;�W�����3��������o)��p�����y���<+,�����`<2�f����{����`�������������\Le�N�����Q�
]K���"b W��>N���=�g����JT���RU����IW������_U�)*��RK�_U�Y���Gg�����g�xZ��b�W��h�����k_�c�b�f��/R6�r��6�gXw{u�Z�����qOau���I%J���%_*���J�U�jE/�W���]!{���H^^��!m�zN��^��:@���T9����������Av��7.>��BM�N�G�mC|�<4�:}������:Cm�{'Hs��{���;�:Co9����o{��
���Hoy���2�w�Hg�`�F����fG�
����>1���6�H��{.�X�T��D6
x��)�{E�s���/���c������������a�vT+����^�]H9���X��Z:��~���;��+�V������*PT9�=��G.�����Tm��|���Zv��Mvn���;�Hxx2/�/D2�+s���dB�ow�;����:�9��}a�q~����7d�Zq����5����P��������)�J�&���=��@i��yz����~K�*
�|��k�T�_:N�:���$�zw��;bdGY��Xx��;���y�8��-6P^��xn�/�0o��U	���>
�4"��$p��T��l�X��"�G]Vh�n�)]��x��9z�� v��a��uA��yz��G<�Y;5.<��4�V�>������S_��c�������T��|�U��X��
��D�<O}�B�=�T��R���xU��0x]���J�}W�����"R��}[�%�*��K0`��g3���9�������#�D9w��n~�n��X����5�wM�C��^��E����'3��Q��x`��9�Hs��{:B����!w��N�����t��)|��8�J��i��c�}c��j�ng���k-t��;(������������'<0x9�=���9�`�������gHn���:C��w�Hw��z!����_T}��9{����E�+�x��sG�{3{�������-���;���$��	��Nf{�<	�������T��D���_U�IU*��jIW���������=5��b������(����M�4�$���F�nK6(�x>�U;l������_V�R��C��;��y���!����t����:C�Ng�x'3�<Y�<3/3�xm���^���c�9S���������<VJ��VVd�����k���s��{���{��He��C�9���'He����s��vs3������G���W��/��:i9��fgV��]���t����C����X��;����kg�	$���x`��9�=���9���&�3������r�3���V_3�Q��s�����1��.��~��u���n\�PQ�}�U��JEJ����{���
��z!{�w�-�9��;o;��I��e��0xV^f{��������}M?��zJU��A�qR����K��>�bU�q�Q`�\o�%
7M5����9��	�9�`��s0�������T����/�}I$�R��sRUJ���%J����E��o�������%���;�Ku��b���j����4Th�/���d�$L�D���LL��H�-'T�2v�%RD��L8C��I! I$���(+$�k8��I��+�����������+gf�aji�j�k�\d�H�l�T���+m�V�-j��s5��B������au��R��T���;���]i��L���eV���[;�\6m[H�T�Z��[[J�6���[u�2���ww)V�d�D�ttse�EP6��0kfh����l��2�U6���jh�*]�[X���lR"��m��&��L�l�(kk4*����5	l��V��U�`�4�fB���hlm������������l�kk�J��**����v:�4L���@�P����b�:�S�
����:
�z������p ���
�
��0=�]�,)���������A�lgZ��Ip=��p�y`7�p<��=�w{������0��6�5v7�n��]g��P�z��W-������n6���;����.���x�����;�����{]��t��	�O��p���l=��z�����
��-c���z-�{{�����f�K�*I�������n��������p;������������P����wu=h��f{5��W���%���`;����]���op7��0#�
�����[�=��5�{=�������0�90u�w����xy�s�n�q������-��w�z����
��������7��3��n�8����������������k����������;c���#��������s�����
��1�����P��T�SP�i�&S�0��T
1E �hhM
Bbi�Q��#
M��T� J���mCF�i�A������j�)	�4`h�	�R�J~���4���>��}}S��������[i�[���y������J�r���Z�X9�wn�M`|��*�%����?~�"�#��t���*���xwU]�����oO��y�m����]y����t�7�;Mx�	3,].���;��3i�6vC��z���P�25������������-%V�D=gG��w���34���n�w�7&|��:��c�^���2��]��t�����N=G$s�F�;�y�~���U��v�i������������t'��y���R����o�X���^����v
�N/$���%I�X�H%A����������������a/���:!�#w��UL�X��>����{�v�Z�z��;vz76o���r�A��&�+�}v�Lk�?�l�%v���v�oec��h�D?4fRav2�d�=|�����
c��D���u�lej�p�I��Al)���Q��wjJ*���Ls�V���n��Qz��Z�������p\or%N��������C��������ot`���02������6���*�!�����bb��R9gy���[t���k<F�!��9����g��##k�2��;j��wy��
b��R9�h�x��V)[]/1aR�/�fG�7�X�$3*r�������iV_^�J�Sf���/��s���Z�UZ��n��P�b��d*�����o=9O�%���/����cLyd)�y�}R�8E������p��2S""w{K�ly�wl"�4�w�:K���Z��{��D�9�o:_�0��4�w�/U�W�7L�������R^H >�=������G���&�7:��L��ko��T�������Xe������F7wEN�P��*��w�Z��L��a�r���yc"�Fk��/��:u���%�u-�V�z���,�qs����D����T���9*������*+�L��:���v���7���J�B�<�V������K
����m?R7���y��3��,���bUq��$RF�n���=��5Xu0."���s�����B�+�����@�m_���i��y�w�`Y��\��v���m�a�Hm��q��)��J�N���X3�L-�
������Y���l���Y�����uI�t�+wTU�o�������t�XO�8{����B0�$m�u1�����7�P�����=�C`�;u��M�������)F���L�R(B�`�29eU����'w��z�t�����(c#��:�a�A����gwF�S�����Z_��:�z�����*�����Y�,�:��icv=/:pKz��=��E�Z1����a��|���H�u�vl���}z������J��|�V[��u�������F��N�*��K8����������Z��j������a�������{Uy��CG�1��MB�W�o`G8�6��ey�S��GA���B��u��3pfvC5k�e��yr!xSP�abjsL�UQ��"�ggf�x�p�����������UHZ�����fx]�J
��Y�^�_5�\�c���B����������if���8&l"�(u������t�U��z_,�K�?wl2��� ���e�OK�1#�e��8�z�%+�j���0 ������NwE��xFt��ep�@X�Y�V)�������U���W:h��V�<�wMRi��������c�c��=>3w�w.�1���#�~F�g����V;��CP�v�x�gmbr5�s����R�-}�[��Vg�{-�[r
ky`���x�����1-���
\����H������c�kr���Fd��*���e���x�eN.�+��j�3/���~=�AV���k��P�-M���7�v5n>~��dj����R�^xE��������]���M��A��,:Y[���t��U�yCK~�d������������!����9��5��)\��u���V���	��U�8j�M���*!�*s�&��;G ��F���3^��5���%i���G��+��7��������G�X�l�t�T���N�&�����t���U��D���yf�@�HfQ��z���n����\��#L�g�����k�������<+;���o_V,��.Ge����|��r��������/�P������xD�=��+A3���{��
*�]�u��]/j����(�yf�������<e���(��=Ii5��W{���n>M�pBw��������������[���)�}�6�8���6"t;��X#�%�P���	S���=w���nN�dt75[�l�r��Q.�^u����k����W-=�1�;[�����4)N"�^��o0�2��<��"L�����]�������3�<����7O�V���C�N����,g���6b[����+(���E�_J�	.��r]��hv�J��W�/�&��-�!�.������z��\��8gq4�Nd�I����&�3���y��]B:����2.*�z��-���0n:�_`Y��Cs"��Y'a�M���{���`�>�8oQ�+�����B��k��r�X�!��fk�
z��O�����<q�EI�g��`[b�|����]��p�\`��T��d��JK���������^�W�xM;��k�hH8)uZ�)����<V0�S�3��&��g>,�5��~hb�
j�H�������F�\f��.��+������l2�t4��"��v�^V����cgQ�_�vu8i#��=`��+��,�ZW)Z�9��`��6k%�3D�5^q�S\�0�H��.��H��(�0{��F�V�����-��)W��l&(�����,����	�v"�IQL��������S���g`�qtm��W7�m�}p�LC��R������Wn���`���d��+�[���/+A/�e*���K�ey0��aDN�.GT����e�����]��N#��(*;X�����j7�`�Z������n���T��l#
�L'���1�.��5�p���$-n�Wx���P��o���w�����q�.%�J�������Mm�f;�������U���ax�����o�fJZ�fh��v��bTrzK�7|�w�A�/^����a�]Ox�xL�B:�td�LFR�&b������{z��27,;��7)��z��������7g]!�HI�7�����S]���}0������w������]�;E���fE>�`����;K�},JB{Bnk����M��)X@��d�V)=^������O=�����O	*��,
]�F�:sN@myt������WX��|e�L(�����S��������c)��/��r$���4`:
�sRp�)����Y��g��+�a"��w����6ot�-~����S�Z�5����2���p[���N����f��)�'2�������(��
7AEm�u�%�������I�.��o
�)�v�����v��[L�����HP'ry�����������#3�w�0w^$��o�r�-c}��X�\v��b+{5|mmwT�D�H��-^y	��,���w�b�.*�
J����U�'A�aX-n���v�0V<��])��j��-��_\��y�G*|�9��������k}��.n�;Lh�����-���
�qr;�����)�rX&�Wvm��wmU`�(�������� ����f�.��I���6�G\w�V��9�-x����k�QO�7����j�Y*{h��m�S�ovN~�^���|.���V����kln-�p�}Lq�(D���m�����:���P��1}t�:����F��^4�c	/)NUR��@���q�Y�rgv�i�u�<5o:���r���,a2�x�V��W��T$�^���%�3i&
���_�.g@l����,@u�7�l��J��=��:��6s;�v�jV����Y�J��s�Z�&x����d1�nc��E�e)]|����{r�9�re����v�������b��d4��^�$��+�N��`c���V������qv�}���0�p�7�s���
v2h&�������~��rsZ�����W �.'{uDIU��L��
�|p��j���]��y���3/�+�=a{�{hl�)4����\���6#���&P���<h)���Yl9�Wj��G�9R�|���������h^�n���#��?8F���{�7�u	��U��sSM���	u�������L=J�i[O��U
����Ox#�$�=������C=\.k�6W,���=���e���	�	�����n�����F��C�~pW�e�w2u\)<�A*�^�fS^������\��	C�)y���[�a���s�'��E��:<���������������������b�P���n��L�G�
�K�Z�����Xn��+�BGR^�oE�6J�
��[��#��Gg��.{���W���s����_u��N�a�M�gr�XoR����0o�t9��T�3r�M��)I��1�"�o@��@�"�7y�&��oY�\���gk�&kW��*���fR
���*M�c���+�	�i�qc%�L�6F�/>��
J�a�s�2�6���n�;�}����v���3����8v���c2���e[q�9\s&Zh�G���V�%&6_idB���`c������2�T�U�����l�M*�M9�nw�}r��Uv���[/�i��t#���9u*:tt��+Y���y���\S��b�������"W�8I��<e0o���l���\�y��b�S{����MPm�S�G@�����5�\��q\�����x%��.�Q�x2����ed���K�[���>��Vm��
��Ve��Z�;$O_�S����AE����Y���;o���i��T�/�������>�D
g���7&�l��v��OU+x�0�G�M�6�~����ww�+w�p�����c}}��h{<�M���X]m����.b�jY#���I�:1�J������i����
�f=��8q�S���l����������TZM�8{���A�g��d�xD�
����u�'�z�c�
�3GR6��({��j�$�F��Kz�U�Px�����Y��S��L�����#�;���YQ"Q�"b�s���:�����
`���x�
���vG`R�'d_M�?eE�}�h�:��5��']2�(���I5��h�*��wn�yk'��g2V��Q����~����`^���S!���=6FW���CzA'I8�����A^b�������kt��s�:%�������K����N��Z{������mU�]�Z�/)���,�w���q�j���V���SF��C��{��|s�)��j�.Lkd\$L�j���4�6�iG�ux���g+���L?Q}�x�L�7JG����_�������%v�f�j��|�@�t�����
"HSq?g�b���h��gvl]�1��7��EE����L��Y*t�C�"���!�7b��Z
k
G��������]�gd����o�g��YB��J
m:����2�0������UoV�O�ia��H�}�������c>&�R}]Y*������{\�rN&a[��6��������
����36��n��[�%���^�1�����z�S���[~�+U�� ��H�<����^p�����}l��u�b�����a���.��� ���9��������.8j�]��;�!����]6�9�d�N��o���|�>���l��Nx��]���P�"������!�g��J��v�+��/K���Y��T2�8%���;d�B�7��^]� Riz���o������/x����8�N�h�����
�q���Vc��������5����Q�>���/`iq�uX����!�m��n��M��m_�5"���t�����7���MGk(�����4vAi�b�+�mb�[��h�0)Y�^J��=KY"Pk
d��'����l�/<=��!��l�aw%�.$�:�	�5����Q�����f�����Z����7K�_;�����
�����)�vVR�b�9�n�!��Spt�0���U�:�vy�R8U�WTm���vG�������
���~�.b�<�f:Uk��&���>g�����Z�S,wVu[��B�6�5[�Z���<���]T7��wt_�����2(����WP����|��U/p����p�Jv��������d��D{/]�$(�o�����x�;p�^\t�}K@7���Ub�u���6y���`�JZf��21��6=Lx�����+�=O��.���������[�Y��}u�T�KZ���/o�b��J�|_s�����(����������p����2z�L[��=���f�;�u��8�9�Fe^�S�*�����!Z���6\��_u�e��c���[��pwE���{p>�8+�*m�*���cb%w��Qo��)mr����j�Y�xa��{
��&�]kwz���Sa��
�{1P���9�\�������Q}(���,����F9��--������=����z\�O$���1S����:U5�rz=����5���<F��Y���;h���PV��qh�f@���I��vf��$����@����W'���s�8nzN����^D2��5��X�x��]���O�z���l��iEx����OE�v<�LS0��p��*���2{�z�A���{��)��������n��W��{���lwP�;��f��,}�<q�g(�-e�!����.�j�o�/).AK1��Y�ob8��t��'��)�x���W�A�}��j�i<#�	��K�c���Y1I\�9X������wv��8}� ]$��-j#��m�TcZ�&�w�v������W���]i�i�7��uH�f'�I��eiu��������������t4
e��U���
���^��9w��>�����[#�����nVl��������Kp�^������K������[|A�,Z�{o��o(���1�!�J��������X�������h7����5s>�q�2��������t���o��r�������T�RoRC�M\!�MQP��w�P��[�j}*R��/Mj���v�SS}�����#
%<W��:f�����1���t\�n���+���}�z0�4z�J���o�L�-��(N����fc�sV���ct��I��n��(�}�B�p�*�>��e86���'�h��j$n�����[�������6\6�W\�Z�i���gv�Y=���u. ����6��HR���u�3��
�������<������Mq4����o���m�K���++�w8�;�j��I:���.��[u|8^&��PZ��*vb%����toYp�#f�:���E��V����`�Zy�\v��[k�[��������!E�b����n_�3��]�>-�A�|s�WM�Y�a��x����
>��oz�rD�w|��$��2����b���3n�{�Yy��{B�<��[�$�H����������5�Ub�J� QlJ6f�]<��g�� �������[+ �Ug�8po<
����a�[ZVQ��WW	YM���4���v;w����Mm�z���k�bi��C�e���0�b������W!1���Yy�������-����[y-���y�;��d�5jV�>��Kq'J��o���K��wi6�����Q��oT�e9YQ���-�����y�y�&\���o��{N&qR��u����n��8��V�>/U�)x�t��&����0RV�[�f�O���W|�F�M���4��3��1w�W���:�a
O��.����T/8���/+(g��m�=�������g����-�w��>�y�4���n�u�������e��&7(���8!Y|�T��kn������A�x������	�-����^vQ�,�yz���dy@�[�<y��|
�������1��-�c�7�I�����d���h�1�:e5�y�,�P;\�/	��*����������
*��}C�vy���.��D��5�Y�g��;�zhH�����	wt���j.�
�'�o?_�w�����|�UF��#�W��vax��lO���'x��vXmM[5hJ��e_;U`���T�(��^���W�*K'aX��g�{���T���n|�d�a�omFu����VWV��#	��j�-{C3)r���y�o0��s����k%��;;Ot�����X��S�p�jK�>����Q�����o�/�/c��jxj �eo8YX�4\U���g�i���rLc����&�����b�o��I�i��_.�gK$)�jxNv����
r�V��=W�.O�o������a�����/�*����
����{{�.�����>�L�]>��;97���P��������"����0L��`}U�5�����]�C����6���T�0f����y��M���aG������{����-�4�
�B�ikw��v�u$_��o�y�w[�.������u~{8{'�t�/k�,9�z_�G��7\�v����D��iVU�h�vI:4�Ye����Uw�9�����m{�e��7���R.b�(���N����=�����lMxI�����z���kr�b�K�������}	�w�$�Y=�97{�N�OJon��h��wV��������V,U��(x�q1@�+P��q�I��n=���-+T�OY ���&��{��w{I�1�]�s\��T9���;�N�=�;��}�8X���|�����so�>��X����fX�!���n�\C�����X�7������e;�~�D72N[3/C�N�>��*�a�.*Y[{�*\U\�����Xs"9�L331�}�y+���y1�.vx3X��T�j�N�f����P��d�>�,�4�����������	���H�G�[����������p���Ru^��a�+j�W�N������\���4�������l�9��u�{��dF����Eez�ju5h�S�Nk���W/0F���<~(��������L�	�z����u�y�l���FN���}�0Yt��/�~�u*�Q���f����>N�NY�W5��Y����4[J��]����F=���t�KV{�Q�F�.�3��"�o�8b�%��;��"X��j��|���mm<���E��&S���l���U�7#���_�/�oj�Cs+3�����=��S�\S���;a*�:=X�#2+sIs+s�����"���f�o�2o�Gc;;�ds�����k�W�7� ��������X��R���UB�s�=���fud.���69P���7�8K�bi��ee��U��p�W�����5-���A�b������N�.�]�5�s�kowiwP�,$�p~K�����Z_x;�*mV���2t4�0�������,�S"����6v���3�	���{y�:pn�8���6x��*X�/=;��Kq�u�V�����������K�����gqa.C��w����|J���j�z�������
.)-M��V��}bg��=���n�V�?�NF��]��9�a����-W.�a���L����3E�s(y��{*!6�L�f���f�}b�d���������r�,7�T��[��S\�6��B�pj����{���]�7P�f83z#��H8�������,N;�Z�1a)\��=�~�y���$�t�Q����ne�[��@Y��gw�xKB^l������]*�B�T����,/}���Q}���	s�8�a�;o�������R��h�~���������Y�h�i]X�t�/#�I�.{V�=j�M�co�:|0������/\��[��c/`����.3��kd��$����Z�{D�u��Dl�f���`w5K�\<rO�"�z�������w$X����~�f�����d�{�����{�u������z���4 �M�yX���tCU�iy+��fF�u���>��B�������G^����1�sy�q��Y��8M���Q*49��������$O���_=
E4n�&����i�M�B��FA���������=��{&^FE�r��a�_�@�������9���!z���,a�)�]��qA�\I}Q]���c!�;�����0�����r��Z��P.��C�{�
�xA�o�z*���R�{o�e����=5{5z���-v��"]�K*
�]���������Ue�V�;+�c�ug �k���m�����5:��WZ�]�tz�dUE�����}5gu�b�,���t&;��t�R���Q�A�U������d��ur����W�k���{�G&S^2�,�IS��y��'i{*!.�������(y��KV=�9�Z���(�b���w�W�.G6"�V�A�f�-�6d=��u���7X��H����1q���Z+U{O$�F�5� (��U��1���X���]�.{��;�����`JPus9��Wpne7�5�N���
f��j4�s'c�J~V�S�,��������5����
�d�mA1�.���NBe�g��X\m6��+��7���,��q�U���/�������_/D�wuL�D��>����
0��J��5���:M��Y���R����.'����eo@|���=�..��3��U���1;�T�=t��Dv7A��6��j�{#S5�[+6bd�F5�
�c����B�	�"
b�]~�Ihk$<��6/S��b�������Ol�it��t��KW*�s�
�>}{�������j�7��g���D��(^{�������H5y�=��F7���8��H���ev�#�?<�1�d�
�G6v�+�U�k��X�\>����-pf.�u�%�=Y����@�rz������6�eywj'��'��9����k.8(�����J*V{+!�A��[�y��B�`�����wZ��WLq��C�L�dca��%�K2�U#��\�����C�5������34#:����X~�n
��(�����u�V�`:$yy!h������2��Nv��u�k{(.����eSb�-��2���,is��W%�e\{����s)���<=�M^@}]�K�
V�~����{��5,'���}G�8�mkDo1:��H�:���P��S�V�����s.�l������������F�N_i��w����d+.#�|��%�#�����Ec��{�9mI�O
=4wS�rz%I�������{.�,��z~�_
\����'
h|hA�3X������^���pg������n�6�9A����������0y\���\Y���\��U�}�V���k#K���X����V8���^D��WT����
���v�����6F��i�^���
�4��+i���_�	��e�h��S�����B���jL�����P���������v�]*����n��u���f{vY2�Jz����L�P9�f�A�+��w�f��^��tyu��E�X�]���1)z�#�
��m��<g��yXe�����kj\Y�.C���ceyT]lf�)c��BZTo���KVe��>��[8��D���D�	������
��B��d�j�y���{lL�����}u�8���5[����u5<2�`Y��']<	��}'7(a`��F��_4�5��N����N�����|���=p����r���5[����Yn�.���*�f��-�s��_�o��T-�}�o��$f��\��7n�PY^
(�gv�37}���u��7�9�����B� �����yf�rA�|s}�G��;:)S^���������|�)�C��]�.��O>���9I�*>v�`]����M�u��]�^V#uiI'���/IHZ'�cn��WFU[�8���3����)�
��xl�I�R,M<���n�9a�Z��xq�����[����Q=�/!�f�xwZ�F��~}Me���=:����Ga.55���n��Gst���m��w-;������e��U�I���y��n�e��w������vp�<0��ml��C��\���UT���m�{�}(_�[���O_
�O��*'�r����������W��@��5_��>�q�w<>rh�I�]ax'Zj�Tb�����/��I9+�wv.��R�+�<�X������F�c��EN�p����������d	/��w�p�Qfsw�b�c�kX�9�D��'Nm�F+��eYlq��|�G�9�=a/u��{]T����82�����38a�;eSP����^�!3<m�'��X�z
 ��em���n)��J\�D�^O������iF�i��Ow���/���>�����DV�b�V'ob
�W\����I9l��)+���zl�0���;�5�v7:�Rf`@?9����|����v��9G,5����G���Ir�e^u�q���I����F������\c�%GWx�>��y�W}�Yey�m%���M��BJ�����b�5�=r�����{^zp��7�w#���S�{8������/��|����[�x���n��s�$��8)�k��w���Z
��{�0�
�yF�
��o�
��l
w���e���9�q�5Y<l�5Wr����OR*�>������n����*�a�gD����r������NL�Y���r�q�����.�N���
�O�\|GA�E[����")���^����r��f�������������a-���P�^�>����#�w����X[d��t����t�E���ld����z]��Z�7�:KG��M��>����m���^����f�����i�]UG��c�hPf�������F�HSlw���u[�g1��Y5�`@���#S�1W��?^�Q���KE����7Y�56��8��y����<'u����s[�y?)1�����������{7��zy��\J�U�����Y���U�/%�"��}[������n�����,�8�[v�S�<��`�4FpN��J���U2�3^�c��m+��V�F>Q3����t�*^un�/e�k'p2��8R]~t*��)�LG��z����c���b�����]��fr|0w�`ch?P:���i����f�j�o�f]�{u^7V.55��[K���0�'�����.e�����������!���������6���^k�w<wuWQ=vS����[���Un��n=�b�y��[OpV��ImY
A��x)������#��z�_UykpF��U����PMbr�2f��V+���^��������:�,L]h_N��M�����l9�/��\;��2+�v�0�w��Q���8l`Acn��q���:VG={���
msM�h������S��=��43,\v9�Bjv��uE��M&�O�Ga5b���t��'����QZh���
5gc v���l�&�.�f��3zck2�����l��]>�u���
��	����X��=B]��j����6=��q���2h@|;}@V�f�0��t[�)�������R��*��v�	hp���
�#y�W�T��$t�
�o j�������!���Q���y�6�r�X�������q�t�<��=7/�2��I����[��K~%H�K1)U����1�+���';)Mi��j�n��

�j��z��G�1�wl]omG8��t�2������Z������g���f"f)=���9c�Q���ph`6Y=6�6
�&���t�S[�e#��8e����S�M�z:�1����	��a]���MCE+X�b�^��y������s�c�B�
�T����\�)�q�C�X��ZoUv��=[��^���}{[���(����'yf[�8��@70,�C�|x[}5N*kmp�Z��}���nl|�D�V��������LL5'�@�>�z���]w���d�<9���3n���#��f-���~���k|��~]��(.��-)�&��8��]���!��@�E<��T�]��]�Z�1�F:����j��]��(�Oe��NO�<���f�i�udM5~�=F��^�	1�N������I�(N����/Pdd�~��C��K�h��wZnn'$ �����,�=+1����X
��L���>���c��6�.������#^�~�
IU�����,��"�|�b��������=��MZz�V
c���4��jjw����V�@��{��a�������f0���B{�Ay���p��yX�c�s� RU�5����m{zYe�5{�\�h�'W����2;/(�%d�K
6��Y7��VD�W�4c�����1���p�>�L�'���GV`-��&!��p��m��4��w:?I]�/���g�^o��~���N��P�����`���w�R���P7h !����	��]���c�p�H�4�-Q76�B�(��n�q��-�)�k��%,�q��/�3������"��Y�6�w�s�\/�+5L����F�U%j\��u������-T�*tg���K|���_u#�%��2����VI���7��n&o�tcE���U~9��z����/3=����,YfGZ���Wt��&U����|�qlDN���^
j������J�q�4e��=;k	7��;]j�@��}v��OlN�V�����UE!���5�	���
i<�������6�����RgU���XMz*8��������	��^�i�4MguB�����l0\��o����o3;w�����^*uBL���M,��v���C����8h���Mz;�BI��L�3�u���k
���^���xU��;�sh��hp������ e���}ke��P��������j�{8t_F6X���=�:?#mo=���u{gU&�����c�T�y{���FN��s�����������+&@]���s��%�9e�2��_5u�����������_/P���w�T���^��+��v�}d���H5.�J"�����I����z�Q9�B4WL�Ij����vX*�����c�]���6��9�(s^H*�������Y��=�&w`����Y��t��\��b�����8��B���i�����q�k_�iOXCb��[)����<c{���������vn�\n���o��������BZQ:����ja�j����8���v\��xf]Z�g^wh���J�&DuNbiR�%c~����\�;w��J���^���M��]`�/K����~��V�"��u���6v?$s���f�/����~_�r��9w�a�&���W��V�wN{�A�*NRQ�C}i��)���|0������|�go���Y��Bp&�����s�G��Un	�0$*��o��q�:w-��*�{������������R���$�s_KYUf����wj�B�����&L�K7n���L��U�~��-�j����pA=�k�l[�63�O�=�����Z�J�S�f�D��b��������\�P=�����%I10���mx>��{��g����������?�
�i���S���x�n+\��'u�����i!9\���S���q�������Ga�P��2!�:�;�\9�|��c&��
��po!����0����=l/9���Mb/�|v������/��QYK��K��;�X��f{���W�����]#�{���t�a����������u���7�y*-+
�q�h���(7�PO/�\��[�a�p������9����S��������t�na�B�]�|]e_1Z�R�;LF��%Z\�&��La������%����]�nv#J�T'�w����A���L�-oY�z��HLI���.�E��O�L��������\��V]=�TC�<�'�o,J�lVz�����6nyI�ww�dV�lv�i�h����HDc��������;?n�%�"���%������>x�_��
D,�Y�v�����o����}Sh ��{��%y8$G�E}h�7����v���y��]Q]�K�v]����N�'�6���wg"��1����3��V���Q�nz�
�.\3�2�v]H���u8%,KJ�J
�����5iC�#�"�e[,���mV��K%z�a��9�O��9��6"�� ��X�A������iS��7��>���D�-�|:�`�!��mm�`�7�t�U�*��`��n���{���>r����DkA�jL�T�$z�;���Y�lSp�(.%���z�����]��V����n�g&��;�
�{w"a�:%���I\�v�7��U��k�GG*�C���c�m1���ai��V��^wt��6HQ���6�����a�K*���E\�5����j}�f��u����*�I���t}���'��V'}7�JxDc�/�9��������%x�z�o���&��-m��5q� ��dX���;;8�}I�@�]��2b�.o����3^��lc��H2!�W�e3u����U��il=�	���8D�f!�
vD����n�Z�m���4�^��P���
x��h�\����o��4a�3��R*@��0�����bJ�J��u����04���/�K�qf�a�|�b2�L�=�x�e\���^O>��RNg��|��o�q��l��]�`_f����j�L2� ���F�:�uY����c�u���/i�-���x�`#������������]�������n/O:$l]���ct��K��7�~^m���l�V���8��]���]9�T������Vf�������9��n�@������c6z7V�bHV!�'���f�N�>�x�dS����A�B�y����iU���������)*�K�N���tu��4S���{�������[p���[y���r����:��s��9�q�^�T�����>m����Op��1�����v�6�YT��X��^��������g��f�E}�'0*s}8SVyb{x:�+>4�o��y���d��S�:.�X�^6����:���F�'��Ot$J���hoGI�dJ�TpM�6���C(�j�R�dm�2���xy�oa}b�s���c���9������)�\�
�V5b��\�����\�������{��cD[��9E���%�;^�nU���3�S*A,]�o�0y"�3C����:f�f�a8��������i���O������Ki����R���m��2�+P���TU��C�[9���C6�I3��nf]����")6gj������������S^:i=O���5U�DM����rUWC�a���4���]s�9��FSN%�t��6�JU0���l_^"�������sp��v���������AH���Q�ezX��>yrR@t������1k�M�����;�T�c��G9c�����i���+�����e\����B�0_A�v�#
��������DIN4DX���M��3Cqm��^l��
����WUtf���]A�!�nj�/|<7�7�t��n�:C��GmOJp��UZ�LuJ$6q*�����u��������f�z�����J��Y��4�����HH��n�:���.�5���Y�S�NO���j�������%����E�����Zn����
�818�f���x��z�yj��I����ey���}�@7�h��
�����z����s�c�t[v��Y�2u�/M��Dg<�4���mW$��X(WJ�h��[�4�sZ0���K��\����C��Ee�>��������b�!�t�Y��������#]�r��WF�Qi�N�����T^X���x�����m��upz!����^-�������b��mW��������NUvlP����
���n���T�&�������3�bt]�����3�1f<����J:��J�IT���7�x�U����]���.�M�4>��5k�a}G���>Xt����k���P�q����17V�C��Q�|��L�m���q��*[��6���*����(����'���yt����z17
�x�M*,��}6PF�=�?P����P������n5����d�I�S��l������wn�D���L7�1	f��g37�}����_x����K�N�,��8_��]����Xs6�����<����qbw%�!v�!%c0��k���"��K}��8��w7��glb�6���R��j�9�z�i�����</�o�{�km;!����
q^h����������C/���}S94�yS��V��K{C�ib"t��${o�l=��q
���b)�$h7SRV�����9��lf\Cj�����>��I2���P4_��r��%J!p�y��W��0b0A�Y,k]k<�-�P�,��^u���^�<D�,�M^��jD�����\��������y���-3{'�8��}�{�p�k�T[�v����)Os�)�2^��]n�S���8��WT�8��]2�_���a��C(��&�/wY��i{"*�iR��W��o�u����*|�hR���\�b�MJJ�5�(}X�3r"��i��oF�az�0���!�D|��52<���>�C}�eu�R�+s��0U_V]i�����HF����^I�����}��s�=���k\;����<�{rQy�����4K/�y&�zo[}�3����I���`��{�
�g���kI>��p24���?<�w�+�eg��G�4w��g���/��{nV��'+��C�b9�3ejnR5�ghl�]��]o�R���b������y�M�ye���	>��>�����
�����W���8J��4m��k#I�w�������UMNPR�9'j�����T3l\mT�1��k2_�!���z��7�Us���UM���{���#6Hr� dS]�}�����X����Q.�C'D�]}�6��b�3Y���k[Tz[.���n���������("`���Z�x=�~�����{��B��[��k/Ww���
����
��5n�^�t��~�|C'���J5�3�L��T����-_A��I��u��'jrN�;���&b������&�L��^���n,�<M������U�},u�E�a����3��B�`��&DY�]������M!"Z,�|�b�����r�*5�jn[��4B�[�mD��=j9d������B�+��>H���N�on�xp�G��5��I&O���E
*���s�o������;.n��m������j��,�k��V�����q&�D���8U#�jVon!|I��~<s����::���_�u����������k	���m��1�s�"�x�N��fv���;�*���u�	�C*r�{�,��Q���5�vr���,�C�/M��������9��4������
D�j�A�aOG��[����y�V�C���Xo��D�^���c�Gb�1�����s�jm��[b�j��[fW�WM��x�����4 ��}W
:�����w@��8dC8���Jaa����dO������xBC���J��x��K��l���|��XJ�d��hf���Y�O��h�#~cg���_�WQ�_���a�yi{��	y��U�-���o�wi��i��-�w�=�q�>��G���5��sw��=�������7��>�h���M�_����$�Dk���������0��o�=��)����+>?t�& �����i�l�8�I�V�p�������5Gm(��0@i���d���|�4 ��2��V���y����qV�u�d5�����s��s|�ib�Z���X0D/��]S>��L��P�G�#�ju�R� =��{�	�w}
vj�a���o8�����$�i�������$������K/1h�]�J�q������~��1��enBL�D���`�]I�:[oD��!Q[X�ST���g�	9��U�{�S"�(�g��$�"br\j��!�Zg���=�E �>�z�b(�Kf�7s��2�^7�"�R��`$H�~���
t�Z H<��:-���P)��8���-��D(�P�%8	����[�V���p�����\�8����������`�-�m2A�x�DnN)��]J��
<A��*�h���V�c�^��1b�s]��zu���t@�b����g?b��RD4��{P!���i��B9���n=�	
��a�*�����v�����m��}y���'P��sr��;.)!��1J@=9�/{v�%UX���-��2�����L�a������������<�lv)�����Su*C��:X��j��b(��R�X�{���0
��"D�����*��B+�J��T�xB}��`<U�[h�����h��]��n��
�+yWI|��J��VE�.��hW��6�ty?���4���+�p��+����px�!�"�^�� ��*��xC�v�A��H�w�
j�#�WF�n�<+�����w�U�9�.-a�M>�	d�R��EB���ru	B��WL_gHY.��������*��(��RR-�@���>�'���NU%���R��OnZ��Ot��V��>�P[B����b��\���~����3*�:��_�UA������F�����W&@$�_Y$G��M����6��~����n���O:�#��"��T�S��Z**��f����B"o.*K�������Ul1k���H8�+f|d{��z�#�MQyPX��B.��\�6�E�����F9�CU��^pgu`�G��M�c{��>W���md���EM'������oy�
����ql��D�c[�(l�B.�#��D����a
���v�n+������
�{���O�nv�M���d�NK����u�3'	h���h��A��	�%���#���P��!+.���'��X�+�4K\Dw�u�s~������CO�f<l(��5�o��u��we�=���������kF52�k#�H(-J��uaI.�����n��n��F�E\&������������;fJ�x(��w����/�F*��'�Q��30���J����S�S(�>��[N�T��
��Ec\;9���/{2��s��e��'�M���d����@��r(����s�p��''C%e���,�6]1�>������s�B�m�p���Vg���|�����M��S���P�s����[��}W6:�dr���W�6���!#{2��5u���4T�o��/�0�>�,��Y6-��:��(����tL��c���Q���y������]�fr
���.%��oD��E����0Uud��U"!�����7$io0��}'2�o���_���+��^
��W����������������k����*�MM��]��!�ml���X5���[�=��vR�P/[y6�4���u	�^��am�#�
�1pJ��jfts�r����>Z�tP_R��"V��e �K����]�jP�Fr��qn=�����N�OT�V.����T��sn�\�Il��8�z�������f���3NT2Le0��[�������ov��J�i�e���I�[!�.5�C���m��f�����X�[�����/�'Tg�P�.���[
�r�.��`����d���]���i���rF9�������3��q�n��(�I��Z�M�z�M����jM�P��F�{
��G�lE��KkwI����*����a�v�./;W]c��/n��c��rv�s��K���vY���;��;&V	x�y��x�
��v�-m_=}��q����n��y��uo�D��tSEb-<��HV�Q=;d<�����y�"w��r�{����	�E�)&m�O[s+b�aQ���:��\��%B�/f�7��LF;���zI���J�N�����{H�q�����_OhYCN��[���nI�2�����:��Z����	I�YB��^��:#
����7J�m���m�5�,�"��7L����� �W���k��Ls������Go�:�c�QnZ|��)v6B����gQ7�Q����v�r�[�����%c�����K�0��u�6En�V�5���"S,�k\�
�h���r
����m3{{Kt��$����x���4o�L9���p��38�u� �����`�i�(j�2&��l��i��q�;��`�7��Y�rvN���"I��.I[7Q7q�����s��x��KtK����Gj���^��Jz����m���ZP�3m�,U�I:Ae��N���z�	������&�&�F�M*�:L���wg�����f����.L�2���}�W�����c��j�d]r{�0��3�5f��5��v����M�v���i�����c�;zd�S��T�&)[��Jd����m�d�a[�TM������	�[������x�MAq�utwn������������
ld1��1��;�Vv�i9fI;s��)���gv�S��L�%������V���
�W��+TQ��rH��H�����9X�3s!���U����������m�E�S������n�����N��re�'^��D��q��k�]�i�|���
�wF������D����s �}�������	����6V��s��E���i��q�6DK�#��CY��Y����f��t��$�m�*��m&���y���J��Y���D-0QO����J�K���3='�yV-�]BS�2H��%i�������g��$z�<|Q�"��xw@���o�+��@���*��!Z�� g��nT�%)��2��K�:q��b^��������3����l�-������!�$j���"�L������y�d����9��M_���1���(����m<����
��u��b��;��\�����[Q�xI�:���%8�������z�^7�o��D�x���x	�.�^q�#���B��~��c2q�nGg����������oV ��Y{5����>A� ��r��UJ!��q������W�����1�L`�2!��������S����u�O�����3
) ��,��-�g�7�����k�W[���Rm�;�O0��A���x���� 
���V6�lS�)]���E#h
�@HH=2�=)�-�)t�/x�x�����>�`�@(�
D��(�>=y9v�����mmu�����
P����hQ<d����u"U����Nj/@B�u6Z�|>!��|��v{����=t����!�Y1'����Q Hs��Mf�Q:��@�����=������]�o�q~��r�
���P4��q��^` �m�n��c|�0n�����������R�os��WP��RQ�W:���)�)����V�T����~+M������/��E��b�5=�,�Q�W��E�y��X�I'���L��1��^�:�>>�)_��%U����v����yH��2��A��\�Hx�U����Ob&��$���Y���	���@��h�sU�@suI
�5;{�FR�x.�N�������4������t���2�=��1�	���U�����$�wg�%a��$(��B)i3F������Ffk�%�-�v��Dt����Sz>v�����>��j��k����
�qQ�![��	������<��s���������A�/
�0�6�?�S����]�>������%fK7!Q�0S�U��E
��hc�t�4>V"n�>j�p,�zd��R��bR� ���^O�@h�����I�A����w`�~R����=jh���y��x�Ma
r��Q������{���6�nR�O�7�1`�_9���W�������Dy�SL�>5��g��V��<��T�{n�iV�*�y�W�f�e�B��;���O�B�o� @��T�H^���W��
��&��x�l��*����:9��TFTe^^���m*�����.�>.E���o�F
�v6�
��Lm�m�<>�+��gw�����k�?z��O�}"
<������Y�K����&�6�f��]�9A�2�o��V^���E�`��E�����c{y����
���5y.��e��}�1H��#f���_l�����i�,��Iv������+]pB����J3�w���y��+
�����+#5Uj��4��{������"��(�R ��cn�3l�*,S$��n�9nq�f:���m]a����&:��R�)�]M�:hF��b����#��e��[�� �k;o�1ki��z���!M�yF���������"qV������'4C�}F�S\���P��+��[�_'�E���tK��9�[0�0j�l,�p��s0�Z��1���g�*�{����G��)�4cH5��K�Lm���@mZ�����������u�������H�[����amw{Z���l��q�b>hUQ YpV�1�J\#>`��1�K��g��1����bi ��G��������bG0�boj��Ls�5:G.<�U�KpI#�,�����q�������cGX,��kIk=���*�w���kvUU����7bL=B��r�;{�X�Xf��$���"���=%9���8��d�t�
�47{*��;��t|�t�op���I���`d�������u������ o����#�.�8�1��~4��L���
�c@���gX���]!�9���^���|�|���I#
�@�|� ����Y�P�1������Jh�!M�%���iy�4+�{���������]���x^1���
�iT|�m�+x�0�f��Uw��o�VzbY��/�W��#�Mz^�2��b������W}�NgV�'e���}���gwlt���y�|�%/f:x�j�(������9G�`'�;b\���-��G�)�X[���0>a�%��|hl_4��������kA�[�Q���K���0m/�X	nLg�@�H5���ev�F���H=��i$���{��w�lI!�[B��\�g]E�[��cb}r�����>�>�[""S�H��`�[Y�{�'���|�c���������{��.�������k����{i#2�;�f)�1y���j�75X��)��@{kj��������g��R�+�Zi�=
�X���DUTa�SA-�8��X�5�h��AlZ���>a�e!���>�1Y��8�����@�
x�i,b:�����K�	*��:���m�|�H1��� Z����1[HGn �T���u�i��D�_~�e�sg2��\Vq���7�\v3B$|~s�z�MwYd>�5��_���yN��hl��n���k����y�����2�:��D�����q�����������r16��wN��+����z��.����i��E�{��%uy��=��8t����H���@��i-��2�$u��P0V5^��I|��M`-��bV�F,h�f4��[�_L`&�q��B����1�5M$�?��$0�_v��g���G9m+b�PkX6��ZI�����y�>����y{���	\�F�P�s���}�k���=��es3�G	x/� HQ��1����7�vi��1��1� M��67����?^����� L` M�� L`n{�g���+M	�H@�`�6 M0@������]��� M0&0&� M�B
��fo7��4*`�6I&�l&����f��}U��k	�	���}�������&�l'  R@@�	� W�g=�])�o�}� R)  Q��
0@�	�� [^m���y��f1��<��@
k�� V�E0��5L>h�CZ`w����!\���kB���^o�@�4�H�+a�c�>b2��!� $
����0���
�l=�W� F���#�l>h�]�,���n���Ok������n���*�:�#�Y�x`��/��L����q���_����D���@@��_oq�{p����^fuL@.zo���k�p	F����-���=y�|��$��T�)< B	���|������w��l���������[;� s��ib)��ASw;K;?S�#��v�*f���K����
j1"}S]`}�m[�/�0@eM�RDhB��cAU��CbIS�z,hHmT�5�hB��hX����lJ��bB�=��k�t���}�;�$l����
����lvge��v�eT��K6���:����go�I������2�� �^�O��r�� �!u��}�������w��;	����3y�^`< ��|�[W�����m����B#���{��r!!l�oqZeH�L`��?k�c�WZf��#�#��@�/b
f�g�$����A������X��$Oz���9����-�u�a�����|�/0H���c�)��#��hI7O�����N�!6�����cG�{�����2��W��������s���H�(�gnB����GM�{g�&�����g���R�|

i7,nA<!U�����*-M��@k�����+������*�o����u@u���M�E���Z/1��5v��x�<��-�f�y����![�����X����*�fk
�x0MD�X��4/4/lH��o��m/����X���u��q�����������}p�$m���K}4�A :���_4���F\�ld@-�Fu��b}�v$kB����X��5�,`����=����w7������R����S[��&Ni��A�1��Z|�{cp��� L�ff�.F��/���*�����/7{����� x�������K7@H@�[���]J����BFU���.8#�#�vU��'T��j`�@��+3�7�<����<"^�����}�	������V�A
����������4��+��
4q��44�I6��v.��vbCx�l+� KM�.4$���>��X�\e0������*��c&����v�>`��F�1	V�� }��.����mC��"�[�~�U���CI�W���l��=��?C��$�x�O����LH����u���Cu�����v�9{��w��#���75�@���gv�b�m�l���7&8<!��}�]��@x<N��P���p@�m��ud^����P����V�`n��N<�j��eJ?
�:�fL��N����B"��{V^�V��XV"���P�z�qA��)���� w�Cg=�6;*Q�	s�_��b�D!�U���F�U��|�+�V,��3^�
��n
��P�,���H<!���*����eVT��}������=}�������&�)������)nq����e�/*�3� �B$��d�B�� B6
k���f���:�������A��QDo������&�k�~�g`��/��e��Q:��\�Pm�e�Z�8����4�~�n��]��x8t���2��6{zJ�k�H:U7���(�@��'���A�D�w��;V��z������$�j6��A�s@�B������l=�I��}�l����]_�b��zf$ vJ�W��c��P��3��i�S>��r��;i��U)�A��'�YQ4@OG(�-����6��+=1:�M����<(���}�����S��6��0&�~�C�Y��o�uU@
��d���"���j�%Yf���x���:����M�$`��jz��^X~p��*��B���[�AV�g�O���l��CM��_R�6��N�26z�b�h�$	u�S�B$�	 ���or�U�X����^�_5��$��c�
$b����{��U��z�+���y]T[��G@���	���2�����D|���2	����I�#?R�t(���KD2�&M��#��A*���;9�Sgc��[��*%��R���z��'�z��"���oe:\��EM�O:le���_67��������*	��B:�vr���?=��v<��ZW��NwLJ7H@�9��4r�H���#���/�Wt7�v6&UJ���P2��@gD���V�v��0[�2b�#�&�
Y\��Z�x�8�Q|��6| �
�J�W*��^*�s;L����F��W�7���j5�A����'b.������HW#����unf���|��b.���~��Z���bw�����,������j�|t��.�);�����<>��0�z���C�N��k�}���Er�x�_�4��S�@x�x�!f��u5U(BE���3�9	�4n*�Sx����+��c����X���l�
�*�;w�;8��^���������������vN��,��R����W�~)_
�l��������K��o���Z3l�t�T��?[�2�k��7���oiWj���.Xi�eT�J�Z�������
�,��R���-����������L�����D +fP��}S �.��H�P�x�-��z��e���5t������v^��?)����!��u��]��9j�s"��E#��2�fwoD&<o
���E:EYz%=�T�l��M��$K��+|h��	��9N�Q;�Y�j����%u��`�g�7"�}K�]^�(��El������\���qx��*"*�������.�l���]�]�h�ps;�;tR[.g]��Wk����m�XajEj������W;c�{�[T�o	P�+5:�:�3#���H�,��xw|��b��9��k�;�]��wH1��-��];D�;x�F�_P��1�zV :�5xf����krY��w��N6��:�w%����:����N�:�6�.�
�b5�:�8�Im���������6��[}�Pi�N�����w�����k����=��b?f	�4!�~�AO�����y����LK�5��0�T����J�������7�b��;Q%���W-��A|����[1�*���hA0^`��$�%o�{�h�|@d
i�P\������;�����f��!5���XF��M/G��w�:G����J`�t��2��|3��X������Y�<8����G���uc}�o	V���m���H�#�����[�U��O���y��5��j���j����2��6��B�����,�W'}����t|�o���V��������v�z�������������t�{��#���Iw��W��u�����Z6���������h�6)��^��GK8����5��0 <oe�=��.�5���
��c��I���u��n\��U�W��:+K��MWwpJ��B3k�{����<��)��2�
PPi�r(�dQP�{3/zu��V�W�@�IxHk}���V �������P��.Ws�����-W�	[�<. {��
�}`����[4=���p�3l���d���(}�d�~X��"�ou6�n�^��w�a����������� &q�{4���JG��B��{��q)�@<(����1�L��m����l�)W�!x�����s;�T��sw�q��@!_wv��N����������B_w�3�]"<#�~6.���������'*������
�rz��W��gz�'W�p�n��-9��CS�����
����n�3I����;&����a��,"�:������f�@�w{�p����#�F�]n�<��N|@�.��oG*���_v_q�3q��� �����������x�"��y�v�����z��_8T�dW�@ ����l]��T�v�]�t� �$�UV��9����R�4{�N�81���V����>������S�Z��b&�B<v�+�8P��K�U��z��g���c|�m�N�����|��0�a���������G��BFLv�{�����(@xE_v��|L���}�:���!���x�1�� ��������T <x�����SQ���<���\M���5����
����]���|�\Q��'1:�����]��V�s��ZXU�p���������R<����>���/��lPN����e�g�td��T���MM�J����T�S��Sk
�rhw5�sf�q��u�>PG�w�w:���1�%���t� �L�v�5
��r�xB���q��'���������@B�+nw{��js�����_p��������/�j/c�@qY�����/Z@�sT�k�\��f*�������:����
��W�H9�@�������$>�)���~�E|�N�oi-�U�����l�n����_������������_|�>��|��
�����FB�	$*����Et���f���,�]�$
����z'�@%^gv&�������>����^hxw����7��c�����0��+����������;���C�n8��x�O��J�iq@{� r��������.3�f@�t������u*n��A)����:j�fwK4����|f{/e���-W}/6��58� 'w�_������~� ���v�Fw� ���S7��F�����_� m��}	�����������	����<���� z���z2��2��$GN��u���� B��[������Jjyp��7n���\�L��F�����j���!/[T�#��y=ty��}UO� ;��'����B���Fs
���n�m6�=��Q
���q��:�Q>��CZI�WU~��������)��y �<SR����e�bB"[�����+T�x@����;�R�	9���/���x@�
��oy�E^���x��� ��@���v]�)R�@ �I�1�����p���D�1�T[�l�j����y�U{�G��%~[^���^�Y{c���D��NG�k����U"oT���r&+#��Wz�������gw;1��5Y����yfgw(QJt�	���w����;� =&V��b���{���������<�}4�Y����h�{m�xwI� &�ww��)�,T��A�X�i5j@@		d��wD;s�kL���[��o ����A���7C<�o?.JV�v����CjN��X��xOe8�;?�8	w'��$
$l���'�p��'��v&GfFosw�}UxB����G�A� �$�',��I8��O�k=�yL�b6�P'��wNA&��!�E�a��);�|��:�5��W<��$!���B�@d@�&}q���2�i�j����K����CR&������k�":���A:h�����'��V��.�uG���w��I$�j!�G�}��d
��������UT���R����l�� �c����(&m1� �M����0�
�qV���:|j��#_�dI5� ����}�=K���K�������E�E�/��Kn*o��GJ��(���6;�m6���T���v��R�J�-��{����aB�|�5�'���P��p�M ��j]����s�&�[V�A{k��N�l�(��_�	�������Q�����������#� ��y������������&��H@�oB�<=�L�Mz���.j���=F��!&���'2� ����U�,�]��@
t���!��`�7`���Wk�
2��A�	 �I�y�R����a���-�}g�uWV>�I��1^�
�_��,st)�R�#�{j����{6Lv�y�Q ��%:$a���}�ey���I@B\�/�8�O�5�Z�'�E��
�^�����|���^l����I��(|
��3v���G��3vh&���������*�����[ ��&��� ��)��BD/_}v��K0���}��Q���<�v�:1H���=yr�P����z�/[|�@����}+}���������=�I�V���bz���B��T���g��c/���w�Z��Lb�j6���W��mC���&!n�UA�cz����En�s� <z���;�ur��!���/���W�j4^S��0H��y|'����P'ej�G���@Z@)#�X(y2�}=^��t�iqd�Ko;��Eds�
��:����	�:��vA��j�t5��b�v(�%=������M��
28��� �������y�b~���'|��
/I�y�8�&��c"��n��!��Q�(w�I�����Ap�&A"f�z�&t<�K�:�<
������K������,��/���]��R��p���Add��z�eN�0~F�=�UN�Y�v`�����uk�����{�V��Z��*�/��h�mbX��5;��mZ�Me+*)�'�x�w{;�������F������UB���m�st|���x�d�@
V�P�aH���uzyf�#���]N�Mu���1eT�x�L�=�4Z��=n3�.f�_Wev���W�s���]���G�ZWI*�2P}��6�DV�8I%�}}[�2��������E%�.*����F��pd���t`Y��-\Q������e��4&q�E8r����%��wN�a����x�	��n��@�����y�R
K����]�qK�5����}��|��)w]�AV��u��Z�pQ�b���MY���TH1�Fk�k ��m7@��fZ�k����wy\n�}���l\��u���n��_�r�4���`A=�]Q�V��GK�����������< ��K�h\TO�a�*��'V��P+��^�8�lM����\w!�Ntb�[�i!������|?'�'�Nw�W�~>���_�sD��c���K7*���`zz��+�uu�g��G���������e���Si����,l�t�������b��i��=����j�z��y�s�5�c����H�{���c��0�-��E�p���w����FFT�������-t�
s�sz��8�N.@� ���;�l���������������������$��`���`���*a�����������*��r3>�Y��g������ih���H�A'�����'2��lflP�����Xr n���]��
�+�+/'��A=[�������gc��q�mC���@����.�T��\u�j�Q�@B 9[��Kj�9 ������{o2���_ngr���������m����SH	�u��d���{��:��YO��	'���p�(��u�G"��04�����t�}7�gy�;_R(iv��~��� ��}�`����u��T��tZr�]��
��y��G����Kw���S\����X���A$�}3w[� }p-I'�k��t�d�.E�e�����hH������]�!G�+�����F������x����C����B5���mn�F� �����������o{�j������Z���u\l@@k�cV��fNK���`"���i����=�C/)�u�8%��3��r�
���8S�8i�#��P���]x�D\�������q���inE]I����l����������y��R<7����kZ����`�;y�������m�fb�����������`B����]W B/2�7���������}$��J����WQx�I�������������tR�0w�
�x�;^�5r�w����{�A�����
1�'�Ww�r�)���%�I��8�d�������������*���JP�F���e�C� H�gw2�cm��4�����������#-�os�gw�������f�xM>����$#�������dna��=U7��5������������nj\	u&���q�N�W
��MD��sqM��w�6f>u
lc 6��(\�|/����vde��+!����r�����bE}������ym;vc�[e(7Tb���:�7�������uI.LJ!8����ONG1 !��_]t���N�gt^<���= @Pf_k+=�;��z!s�@��Kz��fnP�H:��oq�T�@�}�����@<�"�w��^�m�b@����+d�3���������GeV5}v���������vY�Ww�r�Y��C����i�zj�Y^��]^JdN(��>��Lq���7n9��(qK*�b������	�r��Z�7e��l�tSo�����6;{{�iw0�(I�@ twgs����V� @ B:�s��o#���<9��4�����@������3�O�<����teRT��x�U{rJ6�����(U�T�m��+,b��Lm�_���u��R�8$r�x:��E��X.}S�|���.U�A�`
��Y��r��\7����������u'�*������bT�����7~D�	�@�+2������Pn�y��Wl�|Z�!^px�����[�v`��*���X���3[���%�=�x����!�A^6B���-<!�7/{��^���!
�[��l�+�h� #v�7�u'���@Z�oD����"u�R�}�m������XIOx"��WP��8�����]���_�j��)q�U{y�{!^���X��)~�J
��������������kG��������}n��+n���n���'���L�����������":}>�w7o�u�;� ��n{�����B`&gos����B��������0A]�����DMf��@	e�f���=�	�(15������6�jf�b�����w%�_p{��t�/��)��$_s��z�������mM����i�!�_��z�U�s|�n��3\���L�����Lb2�9�H<!5��Jqq����3�o's�T�x�fm�q�}+�| <Gu��7�s-�!���}J��r�m�� A���\TD��������������	���l��� � q���s�y�<	z��~���LdT�,mu���}\��}}Pq���}�:>4���:��M���M��J��� Iw����8�{T������7������X�m��l��u����{�WVK����2�Af��`�D�������H�N�6�L'����X���fs��=cLA
�ecf��$���
:P ���|��UT
�j�-�����o���$~ �CK) ��������bYk�y���sE��[����|��r�[�P�����Y���=��;>�g	��>���@\BB��3��P2�#�i�!�re�G|��yE�^���
\��a#��B�������J}%� �b��Y��~�*��~��
p3��W�Yv�e*��a�e��++9X�W�������U]@�D!P�M�����_�b�v
��cb����1�����wv`�y{��A���D!��%��$����+�����R&��/���"@jA�2T����j��J:#	��S9)��������H�.�_�@ B�37N��\*	�V��#����!	��8y��qg� \sk=t���m*{��t
����"c�`�S$b��@�\�������N\�������Z@k�Tx��.�ZtQ�����gBl�<��YV�FdWs�0,��������K�
�-���5;.����S����\�ItPt���/M��M�q_%G���
jbv����l���6�PR�i�I[�����������r�C���c�	�f5��3BA�;��x��L��R\2� �	M
�_U]�(��N}���74_>�������}��/��ne�WU�����������"�9�NDSZ�u�H���v�{K���q4��iS-�G�rU����YE��B
���|_h��Z��>�J�|
 �����:� {�����A��U� �$A�uC����d��sZL�I����t�cL��v��
��L�U�US�`��%V�C�V���	�w�1c����N��ZD9��B;+sx�������'�@$Y�H�
l���v��U�M�=yW=��'�jm}]��!��\W^�S�m���j�c2m
*���^�����G����+�,3��g�cn�a�]Nf�Y����#���U�)��	H��A��!X�,��^��JPS;�M�
����%�H<�RQ\i>""$PVs{9��&eF�u�<#+r�W���>�ff�w��@�n)'2�����G(���`�uKXs���EJ����8�?R�Ld�;�%W�@!_��X� �d@ ���f����Dk
����W]5�l-�����9��m�v����jne2���6j�\D��c���8]��8Z9�� ����e��,m�>����#wIJKA��d���e�!�H������6h������4>Y���K��_V�d�5�`��+)�x�[��)���A����o$�iv�4�.���A4nq����%S�yj���Bwe����8�T6��g�N H���dh)vm����6:�p����N���z�XF�>����%t�KM�F�%Z�j'�u:N��Jj\�����K��wS7j��r�d
v����b�ow�.���*�=�������������.������������r�3{�j-��ohPv�����������NUT�~,�h��Y��{����!�����^H�]����}--$H�p��U������{�9���b��"�v�b���=���������Z#�P�L�y}��q:��#�[}��9�yVt�n��VJL<!V�����C�!v_<�%�'�@ �2�[���ws*��x��y��V@��;m���9.pX�9����&�`�+.�>��������Ob6����_n���������^�f�7�����~�j�/��i��oux	��~B�@Zt
���E
�H���&��y��;;��4������K��/�,n�xW�
zd���W������9�,9��!�Q����W3�G��������xH�^W�����
�y��D�D�DbPD���;�w�����_k�wX!�� � ��6!�"1G���f�N����m��Cb�#���8A�p�'�wsm�=!IIF!�AAAF�s��1�=B�A�lCbPDbPDb8�s!�t�{���c���{u>�K�v�3}T5������=}&>�kM���yt>�_|��B��T�pU��c�|S��R���PQCx����=��}���wvm^s}�/m��C��Cb�$�$ ��6!�7�������b ��9F!�"H"1
�r�g{�����u��h�PD���C�D�D�Db{[���U�}=^���Db�#�"�$ � ���fo|o����6!�"1
�r�C�E�f�6��z�;7DF!�Db��"H"1�{���stDb���8A�lCb9���n]S��HA�lC�Db � ��6!����[g�����"B�������;\�}�����b#�$�$$� ���r^=�����m^�z���)eF���{v��`1�H��^��$z��|H�o�����+����l�gs���he�~��jhm��
��C�������{��a�����'���*����$ ��9IHAAA�p�9�{=��n���IF!�"H"H"1Db�����NM�����;�$ �� � � ���{6{�^(x!�!F!�
�lCb�w���w����IHA�lCb�$�$ ��l���m�����6!����C�Db7=����>�z�vx��>A�qAA�lC� AA��no�^���z"1
�r��$�$�$�#������gy���
$$!H�� ��6!�"H#�������$����r�C�!IIHA("=�:�V��}����,<�w��j�����$�Ku����p��]~)��b�h�	��~��z�H���	o\3���Q���M�����b����G,�v�_r����D��"B�C�$"�/���o��yb$�#���8AA�lF�wss�*���tDbPD�Db�#�� ��;���\f��h��6!��Cb�IHA#=����3,i�lC`��4���{�����6�D�Db�!F!�
�r���6�=�����6!�
�r�����77wwg9�G8"1D�Db�HA("1��n��S+6wIIIF!�"H"B�F�7��N�|�\$$$$� � � ��<3�f�s[k0����V��+�(���!�l�hx*Y��������G����.���p���H������zM<������3bbS��|����w3n���w��s��^P/ ^@����!F!�"H"17{���u����yX��9F!�"H"H"B�Cb2��{6I�����lC�"H"1Db�$�#����{��;��D���6!��#�e��n��\������r��C�$$$���n�g�=��6!�"H"1AAA���6���o�Cb��C�Db�!^_;��_�W&���
�}�#���6!��G���n�}�����C�Db�$�$PD�D�Db7ws}���v��x!�$� �"H"EID�4k|����(��t�f���������1�}P��H�l���Q�*����� 6
���g�S~w��m|��c����q�i�uYfNVf����m����~�Z�����;�aD�D���C�Db�$�$�$PG}�f��m��$PD�Db��"B�C��37vg7�IF!�A�r�C�D����3|��`��D��Cb ��6!�A����|��4D�D�$$$��8��#����d���6!�H"H"B�Cb�v�������u~9�#��#���8AAA����ww8�t�B$�$ ��8AA�p�$�$�*�����xYb$�$ ��6!�
�p�#�Y�������b#���6!�AA�lG/}�Y�3�xo�'xM���c7��]����l�����U��G�@�dGorU��v���������k�gB�|]�w������x63��m����R�
�=�3��}�$ ��8A�lC�D��"������<���lC�$!HAA�lE^�{���o����"1D�Db ��6!�"1������yX!�
�lCb����"n�=�Fs���{h�A�qADb��"H"�y���������Cb`��6!�"1A�lG2�3t|69� ��8A�lCb�#�������d��Cb"1
�r�C�!Is��nkc����lC���$�#��"1F_73v�l���D�"�#�F!���#��o;�{�v^�F(�
�-������7�r���3������t���/�G�}3��vm$��p�9����v��q�$7b���s�_N27�^n�7��g9�v����B�$�$ � � ��6!�;��o�������$$$��9v�7��������$�$ ��9F!�A��g�{���z��#���6!���A�{��fl+�n��C�$"�$ � �"1
�|��fn��{zX�"1
�lC�D�$$����f�����!IIF!�Db�q��}�s�O��g.CDIHA�p�$ �"1A���{����i�B#�$�!IF!���w����-�j���6!�"H"H"1
�lC�Gs�fg����f�~��������9�������5j}�2�����u��j�]q) �\��W�����>��)(:%a��h%uXg>�1���s$�|L/�:�sK�V��w�����$�$�$�#���9r��35����b�#�� � ��8A$3}��=3/hDb�F!�"1
�lC�D�{��nS�!F!�
�r����Ew����3��=�$�$�#�IF!�A���f��tC��A�lCb�$�$�#y���s��y���lCb � � ��6!����f���y�w|�#�F!�IIF!��F]w����w��!IHAADb�$����i�}c�{6!�!�B*�� ��6 H�$xRu5�46�������5R��#�q3�+�s�����d.6	��?US�{8�N^�J�y������)=	�3x� |(������4�xlW�A�R�},��j�� ([[
LS��n���zq���?L>��Sk��c�r1{l����~�)�'r��d���0t��s�OWU���#���(�B�d/@dkiv^�J�!]�y�_�3� �! �r�"G��������s=��	���%�������!v���~?-A��J����5����U�i!����Kp��A+y��`�����DC�=�gY���'�C$=N	��s�p�,8�iT��w(n�l�>N{s��U=�^M��l����������+������G�����|o����5;��[�bEx(A��A��:Q��*y,�H��/Pr���Z��o��@�J������1����I���>p����=>����u������8�����_�'6������Z<�����P��.�(�}0���J���Kcs�/ck}e��GM,,�W�V�=�xS��x�/xH	����w/�����4��%��w>5@
������� Aw�����m HT[�0g��GE��#	E��������$�A$J�`������P�b� ��/&i�A �\,'m9At��cf�f ���|����p�������^���<��}.�(m���5����N�4	-;a
�8�Nx���*5�V�f�/@>���	@��&��N"�1�iCV���,����`
��j���r�L�/:�$���,��bUB��P�4� ��@�I;��zj�� ��d�s��T���,?>*�x8��3��&wy`x����������Y�%w�W�����m�#;!�Lt��:iT.S�� MA�*����1H��*��$�CE���=�l�� %q��:2�n��RI12@Vu���i��\�|U�?M��l���q���m�i����7{UH>��qb�����UF�2K��y<N�&��$L���J�7�X�5/�� �n����V�^���Ex�����g���n^����#�s������u�k�}�?�8� Tp�C��AFc�a��������5Z�n� x��g���\$ ��=U0"2%'6���{�.���J��]�gEQ.�\[wI=�!mJP"���������{�������<�e�d��>�$�A*���W���T��/�C����Nc+wzg���l��L
���:��@8�|���������HG�@	��(1j$6=W!+#�g2�v��b`?W��Gi��$�*� ���������x.�����2���S1g����*:1ruCQ��zKFn=���i.����m-��cmn�G��������������t�DvL�b2{tw��L.����8���r�������m�;JA�U�)����+��b'o�k�L��������s��YlL��z��/�8����Z��yS���=������=����9���3C.��+�6���n�yQ��\�Ga�BM��y��<G��%LAl��\3X���+x�c%�26����^2���vm���8^s���g=���a��c)������sk$��9g��G-����������mei+�o�$�$������Y<c�����*en�B���<�y�t`��"��W����6j������t������]C���K�6s��aA����O��.�{�T�(�oe��4��o�����\V[E�NZ�8�g�����-���v�qo�3"����5��d���(P��"^����V�m���$!�[y�v\�L��� BT����*��]7����.x��@$*��2�mu�BYm���s��!�]��l�����!qu������<�xU�����|jg� [����v����������VT�����y��r����|���^���������7.7p4��5Z��}�t^����*�R��
�u�XD*S��R[��>hF��pU,�����+�"w{y�^C�_�!<	���s����%& ����X����Q�@1����>�B��sO{�Y�Q�{t���{�����	0�o���-�2!�8���|�7���*��w<��F��7k�V������������=["vn�)���m�u+'~{��mX�'(
�}i��x�xQoq�S.*��/�XRz� sU��ue�!#/X^��W$[��f�I8�7� G�1������FD�&#�$ ���������@x@ ]N^�8�{B�<����P�gRBHE]�>�(�`@Wm�|�e��L%��6��g����w�c���9�������f�����<	9;�u��q����<�&���B}�Fo����������A��|\�������y3W@zO����eE���X����W+�:��yf#��^��z"���}�afg�<{{����D0B@xFff�r-�D�����������OU����9�@�7w���mP���@[����^�j BB�s�,&�d`xH@�����Y���	D�x�vf2����O���9��`����Nzn�i����=�{���>�Z+hv%���)�T��o'�b��.�wZ|���$0W$�����]�#��k{�5��������dV�oMU���� �wu���2��<G�{��w7�wz4��_c�o�n��� �]�����A� H�������Y^h@ @�����R^�K���n�dH��B{{���U<�TGm���]��`�$-]���C�c��<��.5(����������
c�z�Q��g��t�ly������_lB��R��>��w�l�X����u�6���PJ�/%�8.M��^�yDQ
'�xB�����Ww<�!#�-���f"Z!!�����k�	�^u�kw��d�Ly�@��1��M[W	&.���5�g�c��5c�$)�9Y����2'6�%�G�BU��jXU�9>G��L^���d]U���1O��\|�x��D�g�UU���|�v����q#����2�v�{��E�J��nu� �����5L�vJq�I�zK�w���\�2{;!��H�W\�o=p��qZ< ������u����ni�n�bj��B=w���]�B����gY5}i4�C�}���bi0G�c�m����1V�/@G�]�����Z�	����h�vSq���[�[��O�U���(����X�A��������f�	T}a��H�`�����>�^���~���7���o��74g���W�
��B]��Nm�Q�x�K���t�<�k����Y1���$ &n_6�\�z	���uu�U�[\��R��cr.����G��Fky)����WN�6� �LB@�x��^b�M?�@#�)��:�r��@#�K���@ !�:����q���n�+�U����|�s�
R��%*���#���������qn���2���[�]���v�N��_����J��j�E����.�����{���r�H$H�����������!g\�>aW��<�oV�o��i&��{���Ol	��!uN�>qr�eGs0 ��S{��9�]���@9�s�����!���y<�m�G�B��|�)	���E�������^����yP���P,��/;��>.�������AF&�z�A��,�{��x3����8_�U9RM[����K���9m������bb����w���! H�}�����X$n�^����@�x ���m�;�Vt�������g����w6���%g��W���s|��<(����9�7*K�An�m����!@���n���)0 ���5;���p�9
-�+z�";"A���"xL�Q���� r��=r}��^�/��oI	�V��H�a��r��&S$6����iT�j������o��	���f�"sPYo�� �I�y�MOu�T�'LgP$M����eP�������#m�����/.� s���n������M��}������Ds�J�����i� �y ��ne���M��V���C"����@Q?CM}#���6-��c����:�[�N�L�R��� $������'����x��^�����D��_XK�PE�K$I'O��u\H�����=��[�����Y��� �y�	���~��.L�z�V�~��y"���N\�' H><LA$����|%�������)����(
��r]���G]��3��&	��W��f���s�8���631`�z�N���.���X�zF�����<�
�����Zi������PC�}��N9Zx��]�����~�IC��D�	������%������������H�)B/���������������B���G�FW���/h8�t��/*�]dQ�3�d�^FK��c��-��D�����,H�������l���*���������$�yZ�'H-����{x��[��p�YU�k�j�
f�'�S���	z����"y����T.�T�����{iD�@�����V�H*��Z����	/�@�A��H���0���FRd����V��Bv-Br�}���u�{���_p��$�JAF'����P��u�����>e�W�Y���U�����1�f �(���Z�]��N�j���������X��e�i0��O�Q�P�yT��I��|��O�!�QZ��O����\_����*`�A��"���s2�K���p�8�������H� ��L�c�L[>�H/���n�Q������B�vQ�1����YC
 ��-������}J$����Dx�����O��D��Z��Ys� H�_j��X�%������������P��~ ��tm�V]�A&amT���C��B��9s/n�$�t��NhH"aO�����	���bx-���� G�!�.�O,��#o5#���]p�pTh�@+7���L`������$�vExY�
\��8}�Es8f�za�N����q���>�j��P�y\��K��I!M=�p�6�lmm�^�T`0NnL���d����GxY���e���]XC�%�/:��H�{AM�]}���{�D6��4��� ��T@ @���A���?�8B��G�i��i���\�x�_�
'�{�mY�B.F^o����V(�,���iK�.�����49�]J87`�Ev��\��=��a���sh+u��^0$6��\����=�����8��;�rE[(��W��Pu�QQ��H���F�5��L����Wk7u6��8�.�[���#l�	��b��g�m���b)��T�����C���JKF����6&��x��g��i���-��u��/������|k�Y!p�8�Y5�e�E���9c&k��.�)u>�8Y�L��"����f��(E�����Dr�����6���
	�2^�����vYEQ��n�Ci�������;.[$�z�����[�O3>Q��o.�X*�:V�oJ��0�{X��q�������
����xV}gO���@ti���l�������U���_��9�e�L�$o�h\��
_vVu��6j�;��n4d�����1]c�Osd��o��=�����>����{������(��y������`  �������&��x�y����c�9R������T�L��J�:��G-��u*xGv���r��!;�!:�����S�g�x-�[m�VT��D��$�;y�B����t;�f}\��^*D��3�����>��TX��l�P�.��xP�j������6��>���u\����S�\�����#����v��_w\�@H&y�=q5U���B!�����7� @#&�����p����	�7�<SQ�i�@���7�N��<�	���;t�:�%Z��sy�3����s�o�����&�����-PA����K�a�+g����!z<�
����'�U!U��/�H��__�Y�H���w�{3��3w��/��@c�n:j�V����@Ea�J��t��O,v�B9��"b�&�s���]l�����|��VuY�B�]���l �� v�6�q���� 5�6��:�����x�	�x�����`����|���	����������{���P^53@�/�ws�7}�V&H �	�4��Ic�����z�W��s=;��m�S�K���2<4���i`���0�����������)�r��{��������"�&0�
��������$�����P�@
@�]���HB�����z���|����_s{��z#�!���uOM{u�@C�>���g"f��on�x<��������Mm_v���� H}}������!ns���9W�1< �[�k���'�C�k����7o1'�vh�KU\/y:=���R��<r�������G+>�����%w3
�"�D����}z8mZM4�i�w����+H�_BW{u���];&`<SZ��s���.m&�<P�#��.^�D�V�>B���s���$y@ 'so�7�qw��@BBA��^Ocm���.\r�xi�=oa���H@�����^�����h���aS����Y���X-�����^�o�zLWps�,o}Ts�^������_L����A���������u�=�Vl�Vn��sM����rD�z���b�=� !G>��kp�h���[�j�S�������E���F@$ ��n6��k
��w�� "y�ok[\��6S������`@��fo7q���G�\���������|���l��)��N�S�8�}s���N�b�0�b8d)�������G�������H���2��v���'Ac��O�+Ay��K4�ay$�`����qw�6�AS�u/sK���D$�T�}��6�1 <��nw<�0�By=���3��<�v���J��x���o��$����5�;��{g� �9�|�����=u���� BBFf>�%����!����}�]��>��GS]�qy����
��YG����[��er�|JK��y�p^�I;���*WE��
�v��y3q+a!F�%a{(��7~�w�D�]������6���������uD����_n���WK1��}�����*j� ��e��j�.������������ ������8a�^� Unw>"
�����l�LQ�6��{����g��<���}|9�1_+c^\��}S�����
���`7�9^�yos�I_�I{@_���g;����`o��^��R,]7#�l����8z�LZ�K��[����PC���M,]����B$������w5� D����t�j��O�>m8���k�*�y��\��� x.�o���x^���G���6�Y6eM�x�{����3l��4�@ fu^knE�T#�{�;�����L �q{h����F']�;��EgpOf�P�N������j��Q�n���`����/�"=�j���\uNh��{B��w7������ ��E;�D>�'D��vo_zW�<#i��s��R�	�����_ok���sq�<��@��v>l]�;�f������YW-?<{fs{y����Z��/�k�����V @x@�UUz����� � ��y�����x	w]�=������)��L��j>��G\��C���AyY_CJV���S������O��)��P�=U�x�������H0K�7�u�����
�{{����uf��G�u�����o�A7�|�����:vj��^���9k�C��U0u�����[Z���6��d�$�	�~�����x�A/
dA�LcA�I:b_d@��JD�^]���>G�J���I ����ee��n@�29)��l��BI��������(H�l�L�d_E��'�H���$z��r��.��u�m�;�����3.�
��J��g����U�������qu�x�=��F�)���@��dDb����Lb��h-[��UvS���I6� ��i��3.�@0A �0	7 �y0���O���Q
���'��z��[�J�+��y2#���V.�zO\�ESy;1��5�����~ ����5����?#����_�{�&�vwj��w�8���~����������d)0�|��uB4�cd�X�(����<�73���{W9���U�g�����?*-c�������<�5.���2Z�����mx�H$nL�G����reL(@!V�{7%��x@�����W�.������ADx=��),���g��������D�D��w�z�
��k��o��_mU�u'[>rDB�E��>������3�"s�b��&���vm����_*���]�u��]T�����AD���Q`xT�n�����>���^4���)����d�� �@�GC"l_:7v,�h����u�����C��	����T�w�Y�Au]\t]����,X�U��������1c��_����_����&nW�}uc�������1��f��N�
 �>�S�A��C�	3jD@D:���\6_���u��$����^nA��a�v@�<���oG�/�G�J�ZK>�5���J�"tmfZ@��>(R��6�6�T�������@�������a?Y�����g��$���	zhTX;>������Q��H�U}�N�r�S��
�X��it�����������*syL�3�����a��SWU#�X�sP�O�wBL�^}��������HP�����$���;���)HG����"F�q�` �q�dPY��|��&�B�x��i���
����3�m6��>��|x��_Ywg��kb0��^6M��s|����g�`��� �_��}��b���+�����-|RS+\�n~�\�$!z��}j�S*n&`p��]��A��'�UeY�z�r���7������������g����d�HQ�#�=\��r�������t�a��wrm|-��������A�uYCZS�H(M7v
`��^}��<����|���m�/;X���"�5MJ�U-T83���	���z����R|��5�U�I�65�w��Bm�cw�;��wZ�skJ�9Kz�f��D�b^�������]:`���9���m��c����23w[f+7]=����D4��w�A3U��!P�����4���le*z��(�$��t3��Pr��{}Y��|~����
b0�����WS#V�����POa��0y�����]�;�
H����\��b�P)���o\��*@���y�K��\���de���r���LV:+L�]}��5)�� ��uQ��??az�?F��_��o���:�
��[�*�����U:���(^�g�W�����U�c= �����9E�-�G<�|������J�����-�*p4�
][���oo����A�R�s�+��M��@��m����<wgkn������!��f�za�� �!�}��y���;�<�AUx���f?U(@� ��m�v�d}j����o�[�qS��B���^����b@!����M�V�Df
y��RJ�f��Yv+�a}����{��Y��k�U@<��o������}0���{�e��j^�fb���)����o�����d�(
/q�����sLu��t^���`�x	T��o<i��s}��'�Bj=/z�@����y������"B<�oy�6�� G���U�n�<�^��9`,o�w9����h	*��|�Q��^��]�6����G�������~� �BBS�k�����y+��W��v��������)p���^�I�z���3���Z\>��pJO�����+��gy���!�����
Xg�����]���WV������zi����b���� �����w5t�j�T��>u.r'd�g������c��N��S@ �w\��)��O�5�}������<�x�����[��J �yw����������|�2zr��$i�u�c�|_G��*�I�$|u�5��k\�\�-���{�����g�q/��Z{@�����_�O�=�0��2R�_Xy:g����d��)W0�����^����{)]v��p�O�V}�X$�	$���i9��{xx� �"j��;8�M B�o[�2�-\�!�Fmv�7��u) 6�����LG��}��������y����������<"��6�n�O{b�H�����^�����< B]J���k�m]?��
$m
o!7\n�{����<h��VXp���:�����#���_�d����-�f��J[�E7qi��A�J��N�{�K�<|���v��7$����@�����b��To��$$����f�;����7����C���T�#��2�7��H�#�x�[��Ml����&���f��=/�@��'{y�V_r����sz�m�V���[w�����8��@�L�?�������BjTd�P�����(���|w[�����W�x.G��
d^X������MK�=J�r8�t��.�FH�����vw:�0�+K!���5���^[��<��x����g0B@"[��_d�Z�R���H� AS�x��>��h� ������)�@�v������Z@$x D����r��H�N��f��LH@M^km���9< ���H��\t|�|	�=|�.NS%]M�7�{t�����i�B���xKh���q�.�����<�\�T<����C��I��Z�s�L���w[2���j�{���n��p=�5�n��o9��O�H�n�{�k����m�f��r�X �A�Y��6�V�Ug�[9���li�� Wm�ck_��B�n����M���A����{�OigF����|��5A��� �������"���F�k
(���-�n�9��+D�	�s/����p'�x����\9X�6�UG�:xU�������e����krlo��+&:�n���^�������V�oF���}���x:�����"��x<�|�k,��<f��sx�50$��>y|����m�a���wu������x(��[��3z���� �s���YJ\�-*����(���Jv$�|	�Mp�@K�*;�����vf����x���!^���{{�������;����X� ����W��$��!�<�37�:�o3��F':&�b�Wk�Cy�
��]���v<~$����5f1� nU���c���x���m��[������U���[��d� �@?@�V�k[���F1<�m�����t#�x�Uf_s�rN�@:���r�1�w���A��5��Sut@�<y���?�^��J�4����
���9����]�8���3�5�5��[k�j1�w�C�xz��rwj�d��[����S�`���Z9�k8��8���or�u�}oi7����z������ �x����>��������a��H���}�TWt*��<T����GnV]���@!��u������t�@���=���8���b�D�}���)cy	�<$.^�:�r��� @ �������Y���-b�������/�������LU"V�D���a�������!�IA�x`O�tY�H��p+����>���}���$]�M�b��f����n��g��K����G�3���~��w�C�����Pj����Rt�7�n�I�q�(@�=�@�z7i{j�%9R�	�����1���k��c`>�9�4 	 �^9���L����p���+)�����7go�f�����'2�NR���@X�������_��R5{8���3�L����@Q�Mc���3�	���b�/��RF��C�$����kS�Xb���@��v{�����*b[z ��#�[��Q�������c�����g��2��:%�����eu�Hi|	���`@(�0�<�#���HG���F�����r����^���$�8�$��VDI�QD��0��e�y<
�����A�q�["b]�U��I� q�.���8z�i`�%�*<�Pe����_FO�*�h�=��4���DI �8�q�a
�m�����A�"+e|li��`�<.���g[?B1��e���S��^T������������	l{����$QD��uW�������i61��i��U42�>5�������Koh��!x��g��d��Y����P�W-s��$�KTea-g�Lj��48��2�T@�� H�I�eD�v��_��}%���Z� ��}�^S�REFb�d�+G�C�*b�BB<(�w����j�^h��DL��Y�����i���]�����|~����P�Q���S6�?�����(�V9��.�����t����-�����u��]Dj	�88e�@C_�������@
 q���t�K������8�~|m�����}�U 
�l�
rb�?v�	8�L8�?{N���*�����3G�_TM�B���m6�#b���M��V���k=F��`�!5��U_Vgf@����T��t(F���{�VU*B����q�ExY�;������F��7���^��
�]��'rr�Hlc�tZ�R���Y�(��p�`x$l*�o����3�J���'�������iE����!	B��)@o9�7�HC�u/���$�@	�TP����+����f��I8c8�����[��h��*ge�����"F���Wf��I����N����K��T�����o(O�����@�An����U�!H|Pm��G�,��*�k(A�5� �t�T�bD5	��kV�TR����Q[2(���O�{F�A#��3$���|A ��H>%��4_UM7B�As0Z9�ef��,1B��w���-������zY'�<�u���n�wVS����3q��{�����X��o��2j+n�����/D�7P��n��U�5����uj��w�v��[0���������}�"N�|�����	�4[7w��u(0s��h3A�v�d@����`�
��Y�)��C�����l��#�����	�MN��(pV/���v�1�0��0��{��0�����KD���wVov��;V�*
A/#o+&��6��N���v]�zi��|�i���tsH�
,3Y�eX
i���v���+gvb�]����U�1>A�P'���_+���pS��q�����A�i��n0r�����Eu��c�~?<��tUv�z�:���R���"���Fth�� z�(�)��������3=�����@�C���h���o��'��9[������+�pa�U��^\�q-�>�ou�}=�v��!�������^>�"x@Yu����"S<x��V�>m�C�3(�!�_=���[jA���9��7xvD"krs�yf]����E���@����f-,!�\��|��K�Y�`�x��z��R����n�����9}��KP���G�Hqz�Vm@���<*~����p�:��������P]Nz�4���&���+�NS�F�\,���{t�c#m��������G�\��swS�Q���j����}�3��y��{���V�) :�����R�@�Mu��w���@F���m���pB�n���7�+��� ���z�z�����E��o
��������-�����U/;�Z�:���a��+n������W�� �X������-����sZ`��VpcHp�:����5��oM2����S*n[��g���+35�����P��@!�/�y�5<���Fk�o`�h��������s/�X<!��y���4��g��Vno8Yr�=����n�D�@�$�WkwwuQ..�>����<@|���a�}��9Y�Tl]�k��7�.�vP����;��5���(E��.49�b������P�v����w��v#�����
�
v+�w��=�52t�ow�����z�y����Ok���@�� H�[���7+�������)�<|�GE�o�<x��m�Q��7����^�u�=�`!�m�>���\p�(���{[C�U@��f
���������w�k��z�x���U�s/^����@���{����g���X{�-Wo7k�����Ui���
2�����<"a�=��']���iuf���|w�p����
C���w��G���<oZ�����]�)@���+y����2x�^;����F�@@%���k/+&i0� �_w>�b�m�"�y9���mfj� A�Yv�>���y�!F[��r�9��� �$���m�����s�Z�s(;�b�/��������P���~��4�������{�y�.�u�������Y�qd�U��|K�N>2����s9.!��F��Uy�7*���oY���u��BA}���U=�d�G����[����^������m�8���� G�7;�[�[�<���{����~��>!��{�i��B �����\�� G��gvs��@�	on�6�r�V�!�_�������o���fF.�a���rRv�'���z*p��{Hi<��.��YU6�},�&������)4��C��J����"���[�n"�N�gfK�#�=u�7���<����O�<��}���fdVd����gN��q����#�I���{�ww�2�q�����~@!	*���\W	�V�!�=�|���1P#����o�>TT�@ �v�=y
y��!,������-4#L����� �M�:V��G�g���n|��g���"4��h�=�P�����M��k�L:Y����.�f
���5�����Hh��C���l����U�"��< ���m�fH����{�>��f�B<!��3[}�@�/w[s�][�)���o
�Y]0��@i���O�Z�I�f���]^���Px<���y����� ��U��o���7^�.�k�gNn���Q�V�*��#9�c����8�y�yZ�{�tj�.�k�+<��������
l�K>+L�V�5�n;u�\�t�����f���q�F>�y��D�|��kw[���� y�����z����Aw\��h7��!*�s�����O1@�@�tvnkn��c�1�oc��{9�h�*��uY��Z�1z}�{z���& "����uMjr����w^���9����3kw�Wu�c�q?c���
���x7��++"��>��u}����WW`��l��g�K��3K��:�B��@V%W��[��4�c�'�������L@�FV=|�����������|�F���
����..�n���+�qy��� H$ �wx��A	�gT�|���@��({����^Y�AW���6�In�@�<�3/�PT��R������1 W�����}�����H���s������Yij�TZ�7��/�11���|���[A�vr��� �
k�'"x�<��p:��NvmZv��wD����Y��W	���4P�]��i�[�������a
���me��z�Y�B���v]�mZ^�����y�����z���������<Y���U&����dj�e�6��{&�<�18�g�����	B�=�^��B$�����Dz�3���5���3!N���7~�I���|�HB��W?��AM�%��2�!`��Y:� ��L���� P�8��(�
�T4�(�c8�w����>���������K�u�����A��;e��:6��>��A��[q�/�l��l�C�gsa��NU�c�E���6���L��0�J��'���/C���?���w9�Y�a�Q������$�k;���VP�(��wR�t����5n;T^�=���{��mk"h�7��?�$:�OpD{=! ���,���B~$?X_H���,A��f+��^O�@��tf%�=���*e%:p�	���i�'������y�fSr�Y���^Yw��E�7��0�5���������@lm��=�L��H�	��j(�w���������		�,�W��x
'�a�-(
������ �&�vA?t���B�T�	u�n��G�wF�|��:@�n�����^R�
��Cr��9����}9ru���m����"Hb�?bWcX���;��J2�x.f���@�
����>�~�6%�.
�����(�[���	���e�_��#De*$���.QM� �����z5�r�����~��g�@�N���������^<�����! �]-�}������&2e����F��qu���~^U�2t'�B�d�ub��H ��8�q�@#}K���r���P�9�������p�q�r����������nk!C���Fs����Ic���6
|�����{�a��W��of����L�{�P&t�3aID���!���y>� �N���y�!�@��|�f�H��)=�b��H�@(���z��r�u�!��.����W��'v�2�3�=���B�'<������Q�j�M��+�Z�-�[��D5
\!'��xyQ�����S��MV�*����u�fb�q�iK���7�����gv����j�v��i����:�x�&�	$P�
yH}�PUB�lg�G.[�:E�G��&z���Zx�tQ^W2q�������yw���Oz�D*BG��s�0�7q���d��H�^$l0~�(Au��xG�n}>�����T+t�]�rH����+��lw�uYtEC��k��0��7����������A:�������R��%X����{��+7��z79u�;g�V�. ���H��D����|D��V+��kjnnm6;Q<7Z�UZi�H+�Q��:��(v�{
f�������l��j�
���JZ����wR)�gJ{x��,�����k��1����-(�.T�3&.*c'.�>�r-1�H�Xz�/7����I82�P�a
��>�[1�����I���4O���JG�GO
Y4Z�cL�*S{3"J��V��]�n;����V��:uT8oE*AZ�,��A���8����0M]�� J�3�����.��M�df��������}n����E�����u&6���n�H%z���������3�>{">�C��P�,�3��8�6�������x��^�
��+��Jo���>S��l�
����|����G
����H��~����P��LE�f���*�j��������fn���;�� ���m�����7������&$]���:a��G��������y��y��pr��< @���cov�X�$3�s��Z�R�@ 
��w���|��< �7y�����@A����O��mv������7����H����c<�0&8�OX���}��m]�w���s����E��V-���5~}kD|��v:��+ZK��rG}��^�pfX �@e����S��@!�Wz��&�1<x2^�n�>���!�
/+���gQ(B@�>��ny�Ni ���i����f��!M����}JS5J� MU<��=Y`  +vw���^& �H7U�����m��M��Lzm�
c}�����s�M!�}��K��_{����$9n��{�i+�4����*#�z���7���qS��A���F��<�](W[W�n+v��������m��mOZs��HB���[A����Mf�<�y\���B�]�����8���Y���!��}�{Q,�OMws}|���x���WVc������ G���s����'�Y��o1�s��W^�A"��+���R�������]����E�~�j��K�Ut�{��~� ������o������?����f���[��A�X2�����"d�}���;��=���'����1�P �����w11tK���������i�B7g��ouk`��L����wF��B���m���_]�H�@ @,��{�T�Vim@��<�o��"h<V9���Q�h�&6������� �7]����4�����w�
�f��q��d^���J��������f7�<:����P�GS����
�j�O(�PP���v{����G<^{^���H���
�������S����z�nb�P @�{���A����9v��*����4#��oq�}���T��y����4��<�U�w>kS[�������_c�6M�msB\�gv�62�@7���EXv�6_��@em�}6����������{��%���Z5s�����C���8�L���=��)���xz����E���f��j���W�m��RX���vffw$��^�W��nq�/$��������� �����0���
�P� �#i������V���@ ��x��NF�����]���w;�)���c�����/U
�q�n�.����yX����X�@ 	���o������L�z�_���	���;�>������M�������������=N=�a�N:����]�r&{��}�W�����bC����NJL��#������B��������o���5?D B�.o}S�u�B���o�J���@�$/D�f�8�s�|�� <A.�9���[i�<�<Tc��{��B �M��X8�Y2���@!E^gsfn�K���y����J�x�@<k����M��xo��-��=|�]9��fU�>���=�G��nW�G�e�����Y^��U�U^]��^��%d��2�=u�h�m_7H�
aV�9�!Z����������S�^J�,�S����a��k�
�P1g>wT�]��@�evg7kYk���G�A��w�������B(����z�j�G��D�����UE(�*�[��.y\� ��A��y��9\� 1���k/EhB<�4���zk��A�Q�yS��6q]'\��]>V�L.�j.-���/������
\P;�������xt-��nV��H�'�t���k!�2.x(��������W����I3p�{���^��T�@�E��n��p�����y��m*�e�����.�S��y��A�K�sM�|����/;y�f���vf�����z��B�������nl���� 
:�;y���q7���S���}6�|��r���}K�=\*�U����}������j���Rgs8����4�6��';d�Fq+��9rz����\N�T��!�(���z��������Y��~��<B<x@u�����t�
 H�����J����=W�����)�P���o>SU�mxxx��f]2q�X�xvd�����]M�@ ���7������x<S������$�� B���|�DhZ` ���_��~�~��O�������0��2�}��rS�
'��5��Po.R=@m���L����w�t[(��%	^��E��O!���p�_����4�$� `��1��P�
���B����V�Z�R_��>�����'�x��L�=�,����Y	rjz�rR�iM��Z�.���j_tBE�	���d���MZBxr�+�����g�z�N��;�0X��n�nCQ�

z�=�����hlI����|�S$]<��������6#��$���Q���C}���Hb�Q&����;j�0����,���bi;��
�&Z���B����#��k�y4�����aZ��R�x0�!���V�p���k���>2~�'R�w����)�;��t�Hv�j�����T�TNC
m�4�i#m�yd.��t�M5�
��d��������zP�������N$�L=�
In reply to: Peter Geoghegan (#104)
2 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 2, 2025 at 2:22 PM Peter Geoghegan <pg@bowt.ie> wrote:

A slight variant of my fuzzing Python script did in fact go on to
detect a couple of bugs.

I'm attaching a compressed SQL file with repros for 2 different bugs.
The first bug was independently detected by some kind of fuzzing
performed by Mark Dilger, reported elsewhere [1].

Picking up from the email with the big attachment...

Both bugs are from commit 8a510275, "Further optimize nbtree search
scan key comparisons" (not the main skip scan commit). I actually
wrote code very much like the code from these patches that appeared in
certain versions of the skip scan patch series -- it was originally
supposed to be defensive hardening. This so-called hardening wasn't
kept in the final committed version because I incorrectly believed
that it wasn't necessary.

I would like to commit the first patch later today, ahead of shipping
beta1. But the second patch won't make it into beta1.

In practice the bug fixed by the first patch is more likely to affect
users, and (unlike the bug fixed by the second patch), it involves a
hard crash. The first patch prevents us from dereferencing a NULL
pointer (pstate) within _bt_advance_array_keys (unless on an
assert-enabled build, where we get an assertion failure first). It
would also be possible to fix the issue by testing if pstate itself is
not a NULL pointer in the usual defensive style, but I find the
approach taken in the first patch slightly more natural.

The second patch is more complicated, and seems like something that
I'll need to spend more time thinking about before proceeding with
commit. It has subtle behavioral implications, in that it causes the
pstate.forcenonrequired mechanism to influence when and how
_bt_advance_array_keys schedules primitive index scans in a tiny
minority of forward scan cases. I know of only 3 queries where this
happens, 2 of which are from my repro -- it's actually really hard to
find an example of this, even if you go out of your way to.

Allowing pstate.forcenonrequired to affect primscan scheduling like
this is something that I have been avoiding up until now, since that
makes things cleaner -- but I'm starting to think that that goal isn't
important enough to force the second patch to be significantly more
complicated than what I came up with here. It's not like the
behavioral differences represent a clear regression; they're just
slightly different to what we see in cases where
pstate.forcenonrequired/pstate.ikey is forcibly not applied (e.g., by
commenting-out the calls to _bt_set_startikey made by _bt_readpage).

My approach in the second patch is to simply call _bt_start_array_keys
ahead of the finaltup call to _bt_checkkeys when
pstate.forcenonrequired, which has the merit of being relatively
simple (it's likely the simplest possible approach). I'm unwilling to
pay too much of a cost in implementation complexity just to avoid
side-effects in _bt_advance_array_keys/primscan scheduling, but maybe
I'll find that the cost isn't too high.

--
Peter Geoghegan

Attachments:

v1-0001-Avoid-treating-nonrequired-nbtree-keys-as-require.patchapplication/octet-stream; name=v1-0001-Avoid-treating-nonrequired-nbtree-keys-as-require.patchDownload
From 702387a3a835ae28bb4dc895323843bad08badeb Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 30 Apr 2025 15:52:01 -0400
Subject: [PATCH v1 1/2] Avoid treating nonrequired nbtree keys as required.

Oversight in commit 8a510275.

Author: Peter Geoghegan <pg@bowt.ie>
Reported-By: Mark Dilger <mark.dilger@enterprisedb.com>
Discussion: https://postgr.es/m/CAHgHdKsn2W=gPBmj7p6MjQFvxB+zZDBkwTSg0o3f5Hh8rkRrsA@mail.gmail.com
---
 src/backend/access/nbtree/nbtutils.c | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index cb6b74912..2fbbbc31f 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -1826,7 +1826,7 @@ _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate,
 
 		/* Recheck _bt_check_compare on behalf of caller */
 		if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false,
-							  false, &continuescan,
+							  !sktrig_required, &continuescan,
 							  &nsktrig) &&
 			!so->scanBehind)
 		{
@@ -2802,8 +2802,6 @@ _bt_check_compare(IndexScanDesc scan, ScanDirection dir,
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
-	Assert(!forcenonrequired || advancenonrequired);
-
 	*continuescan = true;		/* default assumption */
 
 	for (; *ikey < so->numberOfKeys; (*ikey)++)
-- 
2.49.0

v1-0002-Prevent-prematurely-nbtree-array-advancement.patchapplication/octet-stream; name=v1-0002-Prevent-prematurely-nbtree-array-advancement.patchDownload
From 3d261105e8498a6309b612419a23cc3012caf03b Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Thu, 1 May 2025 14:58:02 -0400
Subject: [PATCH v1 2/2] Prevent prematurely nbtree array advancement.

Prevent forcenonrequired from prematurely advancing the scan's array
keys beyond key space that the scan has yet to read tuples from.  Do
this by defensively resetting the scan's array keys (to the first array
elements for the current scan direction) before the _bt_checkkeys call
for pstate.finaltup.  Otherwise, it's possible for the scan to fail to
return matching tuples in rare edge cases.

Oversight in commit 8a510275.
---
 src/backend/access/nbtree/nbtsearch.c | 3 +++
 src/backend/access/nbtree/nbtutils.c  | 2 ++
 2 files changed, 5 insertions(+)

diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 77264ddee..e9e53cd7e 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1791,6 +1791,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			int			truncatt;
 
 			truncatt = BTreeTupleGetNAtts(itup, rel);
+			if (pstate.forcenonrequired)
+				_bt_start_array_keys(scan, dir);
 			pstate.forcenonrequired = false;
 			pstate.startikey = 0;	/* _bt_set_startikey ignores P_HIKEY */
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
@@ -1881,6 +1883,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			{
 				pstate.forcenonrequired = false;
 				pstate.startikey = 0;
+				_bt_start_array_keys(scan, dir);
 			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 2fbbbc31f..91ff52868 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -2546,6 +2546,8 @@ _bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate)
 			 * Can't let pstate.startikey get set to an ikey beyond a
 			 * RowCompare inequality
 			 */
+			if (start_past_saop_eq || so->skipScan)
+				return;
 			break;				/* unsafe */
 		}
 		if (key->sk_strategy != BTEqualStrategyNumber)
-- 
2.49.0

In reply to: Peter Geoghegan (#105)
2 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 2, 2025 at 3:04 PM Peter Geoghegan <pg@bowt.ie> wrote:

I would like to commit the first patch later today, ahead of shipping
beta1. But the second patch won't make it into beta1.

Committed the first patch last Friday.

Attached is v2, whose 0002- bugfix patch is essentially unchanged
compared to v1 -- there are now comments explaining why RowCompare
keys cannot safely use the pstate.forcenonrequired behavior (in the
presence of a higher-order array). There is also a new 0001- patch
(not to be confused with the prior 0001- patch that I committed last
week).

I plan to commit everything in the next couple of days, barring any objections.

The second patch is more complicated, and seems like something that
I'll need to spend more time thinking about before proceeding with
commit. It has subtle behavioral implications, in that it causes the
pstate.forcenonrequired mechanism to influence when and how
_bt_advance_array_keys schedules primitive index scans in a tiny
minority of forward scan cases. I know of only 3 queries where this
happens, 2 of which are from my repro -- it's actually really hard to
find an example of this, even if you go out of your way to.

The new 0001- patch addresses these concerns of mine about
pstate.forcenonrequired affecting primscan scheduling.

It turned out that this unprincipled behavioral inconsistency was only
possible because of an inconsistency in how the recheck within
_bt_scanbehind_checkkeys works in the presence of relevant truncated
high key attributes -- an inconsistency compared to similar code
within _bt_advance_array_keys. Now (with the new 0001- patch applied),
we won't allow a scan with an "almost matching" set of array keys to
continue with reading the next page in the case where the keys merely
satisfy the next page's high key's untruncated attribute prefix values
-- we won't accept it when there's uncertainty due to other arrays
pertaining to attributes that are truncated within the next page's
high key/finaltup.

There is no reason to believe that this matters on correctness ground,
or even on performance grounds, but it does seem like the principled
approach. We shouldn't cross more than one leaf page boundary before
resolving our uncertainty about whether or not stepping to the next
leaf page (i.e. not starting another primscan) is the right thing to
do.

Note again that this is a very narrow issue: it could only happen when
we advanced the array keys on a page to values that just so happen to
be exact matches for the *next* page's high key, when there were
additional keys corresponding to truncated suffix attributes in that
same next page high key. The chances of things lining up like that are
very slim indeed. But, I find the behavioral inconsistency distracting
and unprincipled, and it's easy enough to just eliminate it
altogether. I haven't formally promised that calling _bt_set_startikey
must never affect the total number of primscans (relative to an
equivalent query/scan where we just don't call it), but that feels
like a good goal.

--
Peter Geoghegan

Attachments:

v2-0002-Prevent-premature-nbtree-array-advancement.patchapplication/octet-stream; name=v2-0002-Prevent-premature-nbtree-array-advancement.patchDownload
From 98813d0b0f6c852e5117a677195c8f1b9471a9a9 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Thu, 1 May 2025 14:58:02 -0400
Subject: [PATCH v2 2/2] Prevent premature nbtree array advancement.

Prevent forcenonrequired from prematurely advancing the scan's array
keys beyond key space that the scan has yet to read tuples from.  Do
this by defensively resetting the scan's array keys (to the first array
elements for the current scan direction) before the _bt_checkkeys call
for pstate.finaltup.  Otherwise, it's possible for the scan to fail to
return matching tuples in rare edge cases.

Oversight in commit 8a510275, which optimized nbtree search scan key
comparisons.

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-WzmodSE+gpTd1CRGU9ez8ytyyDS+Kns2r9NzgUp1s56kpw@mail.gmail.com
---
 src/backend/access/nbtree/nbtsearch.c |  8 ++++-
 src/backend/access/nbtree/nbtutils.c  | 42 ++++++++++++++++++++-------
 2 files changed, 39 insertions(+), 11 deletions(-)

diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index eb9163f46..57fdce7da 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -2279,9 +2279,13 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			IndexTuple	itup = (IndexTuple) PageGetItem(page, iid);
 			int			truncatt;
 
-			truncatt = BTreeTupleGetNAtts(itup, rel);
+			/* Reset arrays, per _bt_set_startikey contract */
+			if (pstate.forcenonrequired)
+				_bt_start_array_keys(scan, dir);
 			pstate.forcenonrequired = false;
 			pstate.startikey = 0;	/* _bt_set_startikey ignores P_HIKEY */
+
+			truncatt = BTreeTupleGetNAtts(itup, rel);
 			_bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt);
 		}
 
@@ -2461,8 +2465,10 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum,
 			pstate.offnum = offnum;
 			if (arrayKeys && offnum == minoff && pstate.forcenonrequired)
 			{
+				/* Reset arrays, per _bt_set_startikey contract */
 				pstate.forcenonrequired = false;
 				pstate.startikey = 0;
+				_bt_start_array_keys(scan, dir);
 			}
 			passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys,
 										 itup, indnatts);
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 686adb9bb..94e230d7e 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -2634,13 +2634,14 @@ _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir,
  * primscan's first page would mislead _bt_advance_array_keys, which expects
  * pstate.nskipadvances to be representative of every first page's key space.)
  *
- * Caller must reset startikey and forcenonrequired ahead of the _bt_checkkeys
- * call for pstate.finaltup iff we set forcenonrequired=true.  This will give
- * _bt_checkkeys the opportunity to call _bt_advance_array_keys once more,
- * with sktrig_required=true, to advance the arrays that were ignored during
- * checks of all of the page's prior tuples.  Caller doesn't need to do this
- * on the rightmost/leftmost page in the index (where pstate.finaltup isn't
- * set), since forcenonrequired won't be set here by us in the first place.
+ * Caller must call _bt_start_array_keys and reset startikey/forcenonrequired
+ * ahead of the finaltup _bt_checkkeys call when we set forcenonrequired=true.
+ * This will give _bt_checkkeys the opportunity to call _bt_advance_array_keys
+ * with sktrig_required=true, restoring the invariant that the scan's required
+ * arrays always track the scan's progress through the index's key space.
+ * Caller won't need to do this on the rightmost/leftmost page in the index
+ * (where pstate.finaltup isn't ever set), since forcenonrequired will never
+ * be set here in the first place.
  */
 void
 _bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate)
@@ -2704,10 +2705,31 @@ _bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate)
 		if (key->sk_flags & SK_ROW_HEADER)
 		{
 			/*
-			 * Can't let pstate.startikey get set to an ikey beyond a
-			 * RowCompare inequality
+			 * RowCompare inequality.
+			 *
+			 * Only the first subkey from a RowCompare can ever be marked
+			 * required (that happens when the row header is marked required).
+			 * There is no simple, general way for us to transitively deduce
+			 * whether or not every tuple on the page satisfies a RowCompare
+			 * key based only on firsttup and lasttup -- so we just give up.
 			 */
-			break;				/* unsafe */
+			if (!start_past_saop_eq && !so->skipScan)
+				break;			/* unsafe to go further */
+
+			/*
+			 * We have to be even more careful with RowCompares that come
+			 * after an array: we assume it's unsafe to even bypass the array.
+			 * Calling _bt_start_array_keys to recover the scan's arrays
+			 * following use of forcenonrequired mode isn't compatible with
+			 * _bt_check_rowcompare's continuescan=false behavior with NULL
+			 * row compare members.  _bt_advance_array_keys must not make a
+			 * decision on the basis of a key not being satisfied in the
+			 * opposite-to-scan direction until the scan reaches a leaf page
+			 * where the same key begins to be satisfied in scan direction.
+			 * The _bt_first !used_all_subkeys behavior makes this limitation
+			 * hard to work around some other way.
+			 */
+			return;				/* completely unsafe to set pstate.startikey */
 		}
 		if (key->sk_strategy != BTEqualStrategyNumber)
 		{
-- 
2.49.0

v2-0001-nbtree-tighten-up-array-recheck-rules.patchapplication/octet-stream; name=v2-0001-nbtree-tighten-up-array-recheck-rules.patchDownload
From 5f2b071eb0abca3235980d7ae2630d730a47f9a1 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 6 May 2025 13:58:59 -0400
Subject: [PATCH v2 1/2] nbtree: tighten up array recheck rules.

Be more conservative when performing a scheduled recheck of an nbtree
scan's array keys: perform/schedule another primitive scan in cases
where the next page's high key/finaltup has truncated attributes that
make it unclear just how close we are to the next leaf page with
matching tuples.

Author: Peter Geoghegan <pg@bowt.ie>
---
 src/backend/access/nbtree/nbtutils.c | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 517d37502..686adb9bb 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -2538,11 +2538,27 @@ _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir,
 	TupleDesc	tupdesc = RelationGetDescr(rel);
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	int			nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel);
+	bool		scanBehind;
 
 	Assert(so->numArrayKeys);
 
 	if (_bt_tuple_before_array_skeys(scan, dir, finaltup, tupdesc,
-									 nfinaltupatts, false, 0, NULL))
+									 nfinaltupatts, false, 0, &scanBehind))
+		return false;
+
+	/*
+	 * If scanBehind was set, all of the untruncated attribute values from
+	 * finaltup that correspond to an array match the array's current element,
+	 * but there are other keys associated with truncated suffix attributes.
+	 * Array advancement must have incremented the scan's arrays on the
+	 * previous page, resulting in a set of array keys that happen to be an
+	 * exact match for the current page high key's untruncated prefix values.
+	 *
+	 * This page definitely doesn't contain tuples that the scan will need to
+	 * return.  The next page may or may not contain relevant tuples.  Handle
+	 * this by cutting our losses and starting a new primscan.
+	 */
+	if (scanBehind)
 		return false;
 
 	if (!so->oppositeDirCheck)
-- 
2.49.0

#107Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Peter Geoghegan (#106)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, May 6, 2025 at 3:01 PM Peter Geoghegan <pg@bowt.ie> wrote:

I plan to commit everything in the next couple of days, barring any
objections.

I have been using your two patches, one committed by you and the other
committed locally to my working directory, since you posted them a few days
ago. I have not encountered any of the problems that I had been
encountering previously. That's not proof, but I was hitting these
failures pretty consistently before the patches.

I have no objection.


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

In reply to: Mark Dilger (#107)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, May 6, 2025 at 8:11 PM Mark Dilger <mark.dilger@enterprisedb.com> wrote:

I have been using your two patches, one committed by you and the other committed locally to my working directory, since you posted them a few days ago. I have not encountered any of the problems that I had been encountering previously.

Cool.

Pushed both patches from v2 just now.

Thanks
--
Peter Geoghegan

#109Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#108)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Hi,

While doing some benchmarks to compare 17 vs. 18, I ran into a
regression that I ultimately tracked to commit 92fe23d93aa.

commit 92fe23d93aa3bbbc40fca669cabc4a4d7975e327
Author: Peter Geoghegan <pg@bowt.ie>
Date: Fri Apr 4 12:27:04 2025 -0400

Add nbtree skip scan optimization.

The workload is very simple - pgbench scale 1 with 100 partitions, an
extra index and a custom select script (same as the other regression I
just reported, but with low client counts):

pg_ctl -D data init
pg_ctl -D data -l pg.log start

createdb test

psql test -c 'create index on pgbench_accounts(bid)'

and a custom script with a single query:

select count(*) from pgbench_accounts where bid = 0

and then simply run this for a couple client counts:

for m in simple prepared; do
for c in 1 4 32; do
pgbench -n -f select.sql -M $m -T 10 -c $c -j $c test | grep tps;
done;
done;

And the results for 92fe23d93aa and 3ba2cdaa454 (the commit prior to the
skip scan one) look like this:

mode #c 3ba2cdaa454 92fe23d93aa diff
-------------------------------------------------------
simple 1 2617 1832 70%
4 8332 6260 75%
32 11603 7110 61%
------------------------------------------------------
prepared 1 11113 3646 33%
4 25379 11375 45%
32 37319 14097 38%

The number are throughput, as reported by pgbench, and for this
workload, we're often losing ~50% of throughput with 92fe23d93aa.

Despite that, I'm not entirely sure how serious this is. This was meant
to be a micro-benchmark stressing the locking, but maybe it's considered
unrealistic in practice. Not sure.

I'm also not sure about the root cause, but while investigating it one
of the experiments I tried was tweaking the glibc malloc by setting

export MALLOC_TOP_PAD_=$((64*1024*1024))

which keeps a 64MB "buffer" in glibc, to reduce the amount of malloc
syscalls. And with that, the results change to this:

mode #c 3ba2cdaa454 92fe23d93aa diff
-------------------------------------------------------
simple 1 3168 3153 100%
4 9172 9171 100%
32 12425 13248 107%
------------------------------------------------------
prepared 1 11104 11460 103%
4 25481 25737 101%
32 36795 38372 104%

So the difference disappears - what remains is essentially run to run
variability. The throughout actually improves a little bit for 3ba2cd.

My conclusion from this is that 92fe23d93aa ends up doing a lot of
malloc calls, and this is what makes causes the regression. Otherwise
setting the MALLOC_TOP_PAD_ would not help like this. But I haven't
looked at the code, and I wouldn't have guessed the query to have
anything to do with skip scan ...

regards

--
Tomas Vondra

In reply to: Tomas Vondra (#109)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 9, 2025 at 8:58 AM Tomas Vondra <tomas@vondra.me> wrote:

My conclusion from this is that 92fe23d93aa ends up doing a lot of
malloc calls, and this is what makes causes the regression. Otherwise
setting the MALLOC_TOP_PAD_ would not help like this. But I haven't
looked at the code, and I wouldn't have guessed the query to have
anything to do with skip scan ...

While it's just about plausible that added nbtree preprocessing
allocation overhead could account for this, I don't think it's
actually possible here, since, as you said, this particular query
simply isn't eligible to use a skip scan. It's impossible for any
single column index to do so.

The best guess I have is that the skip scan commit inadvertently added
planner cycles, even though this query isn't eligible to use skip scan
-- I thought that I'd avoided that, but perhaps I overlooked some
subtlety.

Can you try it again, with prepared statements? Alternatively, you
could selectively revert the changes that commit 92fe23d93aa made to
utils/adt/selfuncs.c, and then retest.

--
Peter Geoghegan

In reply to: Peter Geoghegan (#110)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 9, 2025 at 9:33 AM Peter Geoghegan <pg@bowt.ie> wrote:

Can you try it again, with prepared statements? Alternatively, you
could selectively revert the changes that commit 92fe23d93aa made to
utils/adt/selfuncs.c, and then retest.

Oh, wait, you already did try it with prepared statements.

I'm rather puzzled as to why this happens, then. I expect that nbtree
preprocessing will be able to use its usual single index column/index
key fast path here -- the "We can short-circuit most of the work if
there's just one key" path in _bt_preprocess_keys (and I expect that
_bt_num_array_keys() quickly determines that no skip arrays should be
added, preventing array preprocessing from ever really starting).

As far as I'm aware, none of the skip scan commits introduced new
palloc() overhead that would affect simple index scans like this. In
general that should only happen with index scans of a multicolumn
index that has at least one prefix column with no "=" condition. Most
individual index scans (including index scans that just use
inequalities) don't look like that.

--
Peter Geoghegan

In reply to: Peter Geoghegan (#111)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 9, 2025 at 9:42 AM Peter Geoghegan <pg@bowt.ie> wrote:

I'm rather puzzled as to why this happens, then. I expect that nbtree
preprocessing will be able to use its usual single index column/index
key fast path here -- the "We can short-circuit most of the work if
there's just one key" path in _bt_preprocess_keys (and I expect that
_bt_num_array_keys() quickly determines that no skip arrays should be
added, preventing array preprocessing from ever really starting).

You've been testing commit 92fe23d9 ("Add nbtree skip scan
optimization") here, but I think you should test commit 8a510275
("Further optimize nbtree search scan key comparisons") instead. The
former commit's commit message says that there big regressions, that
the latter commit should go on to fix. Note that these two commits
were pushed together, as a unit. All of my performance validation work
was for the patch series as a whole, not for any individual commit.

I don't actually think that this kind of scan would have been affected
by those known regressions -- since they don't use array keys. But it
is definitely true that the queries that you're looking at very much
rely on the optimization from commit 8a510275 (or its predecessor
optimization, the "pstate.prechecked" optimization). As I said, my
performance validation didn't target individual commits.

--
Peter Geoghegan

#113Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#112)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 5/9/25 15:59, Peter Geoghegan wrote:

On Fri, May 9, 2025 at 9:42 AM Peter Geoghegan <pg@bowt.ie> wrote:

I'm rather puzzled as to why this happens, then. I expect that nbtree
preprocessing will be able to use its usual single index column/index
key fast path here -- the "We can short-circuit most of the work if
there's just one key" path in _bt_preprocess_keys (and I expect that
_bt_num_array_keys() quickly determines that no skip arrays should be
added, preventing array preprocessing from ever really starting).

You've been testing commit 92fe23d9 ("Add nbtree skip scan
optimization") here, but I think you should test commit 8a510275
("Further optimize nbtree search scan key comparisons") instead. The
former commit's commit message says that there big regressions, that
the latter commit should go on to fix. Note that these two commits
were pushed together, as a unit. All of my performance validation work
was for the patch series as a whole, not for any individual commit.

I don't actually think that this kind of scan would have been affected
by those known regressions -- since they don't use array keys. But it
is definitely true that the queries that you're looking at very much
rely on the optimization from commit 8a510275 (or its predecessor
optimization, the "pstate.prechecked" optimization). As I said, my
performance validation didn't target individual commits.

I initially compared 17 to current master, but after discovering the
regression I bisected to the actual commit. That's how I ended up with
92fe23d93aa. This is how it looks for current master (bc35adee8d7):

mode #c 3ba2cdaa454 92fe23d93aa bc35adee8d7
-------------------------------------------------------------
simple 1 2617 1832 1899
4 8332 6260 6143
32 11603 7110 7193
-------------------------------------------------------------
prepared 1 11113 3646 3655
4 25379 11375 11342
32 37319 14097 13911

There's almost no difference between bc35adee8d7 and 92fe23d93aa.

regards

--
Tomas Vondra

In reply to: Tomas Vondra (#109)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 9, 2025 at 8:58 AM Tomas Vondra <tomas@vondra.me> wrote:

I'm also not sure about the root cause, but while investigating it one
of the experiments I tried was tweaking the glibc malloc by setting

export MALLOC_TOP_PAD_=$((64*1024*1024))

which keeps a 64MB "buffer" in glibc, to reduce the amount of malloc
syscalls. And with that, the results change to this:

You're sure that the problem is an increase in the number of
malloc()s? If that's what this is, then it shouldn't be too hard to
debug.

--
Peter Geoghegan

In reply to: Peter Geoghegan (#112)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 9, 2025 at 9:59 AM Peter Geoghegan <pg@bowt.ie> wrote:

I don't actually think that this kind of scan would have been affected
by those known regressions -- since they don't use array keys. But it
is definitely true that the queries that you're looking at very much
rely on the optimization from commit 8a510275 (or its predecessor
optimization, the "pstate.prechecked" optimization). As I said, my
performance validation didn't target individual commits.

Wait, that's not it, either. Since the index scan that you use won't
find any matching tuples at all. It should land on the leftmost leaf
page, find that there are no tuples "WHERE bid = 0", ending the scan
before it ever really began.

--
Peter Geoghegan

#116Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#114)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 5/9/25 16:17, Peter Geoghegan wrote:

On Fri, May 9, 2025 at 8:58 AM Tomas Vondra <tomas@vondra.me> wrote:

I'm also not sure about the root cause, but while investigating it one
of the experiments I tried was tweaking the glibc malloc by setting

export MALLOC_TOP_PAD_=$((64*1024*1024))

which keeps a 64MB "buffer" in glibc, to reduce the amount of malloc
syscalls. And with that, the results change to this:

You're sure that the problem is an increase in the number of
malloc()s? If that's what this is, then it shouldn't be too hard to
debug.

No, I'm not sure. I merely speculate based on the observation that
setting the environment variable makes the issue go away.

I've seen similar problems with btree before, as it allocates fairly
large chunks of memory for BTScanOpaque.

--
Tomas Vondra

#117Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#115)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 5/9/25 16:22, Peter Geoghegan wrote:

On Fri, May 9, 2025 at 9:59 AM Peter Geoghegan <pg@bowt.ie> wrote:

I don't actually think that this kind of scan would have been affected
by those known regressions -- since they don't use array keys. But it
is definitely true that the queries that you're looking at very much
rely on the optimization from commit 8a510275 (or its predecessor
optimization, the "pstate.prechecked" optimization). As I said, my
performance validation didn't target individual commits.

Wait, that's not it, either. Since the index scan that you use won't
find any matching tuples at all. It should land on the leftmost leaf
page, find that there are no tuples "WHERE bid = 0", ending the scan
before it ever really began.

I see the regression even with variants that actually match some rows.
For example if I do this:

update pgbench_accounts set bid = aid;
vacuum full;

and change the query to search for "bid = 1", I get exactly the same
behavior. Even with

update pgbench_accounts set bid = aid / 100;
vacuum full;

so that the query matches 100 rows, I get the same behavior.

--
Tomas Vondra

In reply to: Tomas Vondra (#117)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 9, 2025 at 10:57 AM Tomas Vondra <tomas@vondra.me> wrote:

I see the regression even with variants that actually match some rows.
For example if I do this:

so that the query matches 100 rows, I get the same behavior.

That's really weird, since the index scans will no longer be cheap.
And yet whatever the overhead is still seems to be plainly visible. I
would expect whatever the underlying problem is to be completely
drowned out once the index scan had to do real work.

I wonder if it could be due to the fact that I added a new support
function, support function #6/skip support? That would have increased
the size of things like RelationData.rd_support and
RelationData.rd_supportinfo.

Note that "sizeof(FmgrInfo)" is 48 bytes. Prior to skip scan,
RelationData.rd_supportinfo would have required 48*5=240 bytes. After
skip scan, it would have required 48*6=288 bytes. Maybe 256 bytes is
some kind of critical threshold, someplace?

--
Peter Geoghegan

In reply to: Peter Geoghegan (#118)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 9, 2025 at 11:55 AM Peter Geoghegan <pg@bowt.ie> wrote:

Note that "sizeof(FmgrInfo)" is 48 bytes. Prior to skip scan,
RelationData.rd_supportinfo would have required 48*5=240 bytes. After
skip scan, it would have required 48*6=288 bytes. Maybe 256 bytes is
some kind of critical threshold, someplace?

Can you try it with the attached patch?

The patch disables skip support entirely, in a way that should
eliminate whatever the inherent overhead of adding a sixth support
routine to nbtree was. It does not remove skip scan itself (that
should still work with queries that are actually eligible to use skip
scan, albeit slightly less efficiently with some opclasses).

--
Peter Geoghegan

Attachments:

0001-Remove-nbtree-support-routine-6.patchapplication/octet-stream; name=0001-Remove-nbtree-support-routine-6.patchDownload
From bf7a6c6d55875c043a61db752831f33d87c4c3ce Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Fri, 9 May 2025 12:06:35 -0400
Subject: [PATCH] Remove nbtree support routine 6

---
 src/include/access/nbtree.h                   |  2 +-
 src/include/catalog/pg_amproc.dat             | 22 -------------------
 src/backend/access/nbtree/nbtpreprocesskeys.c |  4 +---
 src/test/regress/expected/alter_generic.out   |  6 ++---
 src/test/regress/expected/psql.out            |  3 +--
 5 files changed, 6 insertions(+), 31 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index ebca02588..679012924 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -720,7 +720,7 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTEQUALIMAGE_PROC	4
 #define BTOPTIONS_PROC		5
 #define BTSKIPSUPPORT_PROC	6
-#define BTNProcs			6
+#define BTNProcs			5
 
 /*
  *	We need to be able to tell the difference between read and write
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 925051489..410561710 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -21,8 +21,6 @@
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
-{ amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
-  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -43,16 +41,12 @@
   amprocrighttype => 'char', amprocnum => '1', amproc => 'btcharcmp' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
-{ amprocfamily => 'btree/char_ops', amproclefttype => 'char',
-  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '2', amproc => 'date_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
-{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -66,9 +60,6 @@
   amproc => 'timestamp_sortsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
-{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '6',
-  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
@@ -83,9 +74,6 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
-{ amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '6',
-  amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
   amproc => 'timestamptz_cmp_date' },
@@ -134,8 +122,6 @@
   amprocrighttype => 'int2', amprocnum => '2', amproc => 'btint2sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
-{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -155,8 +141,6 @@
   amprocrighttype => 'int4', amprocnum => '2', amproc => 'btint4sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
-{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -176,8 +160,6 @@
   amprocrighttype => 'int8', amprocnum => '2', amproc => 'btint8sortsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
-{ amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -211,8 +193,6 @@
   amprocrighttype => 'oid', amprocnum => '2', amproc => 'btoidsortsupport' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
-{ amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -281,8 +261,6 @@
   amprocrighttype => 'uuid', amprocnum => '2', amproc => 'uuid_sortsupport' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
-{ amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index a136e4bbf..b02ebd1f6 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -1592,7 +1592,6 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 		/* Backfill skip arrays for attrs < or <= input key's attr? */
 		while (numSkipArrayKeys && attno_skip <= inkey->sk_attno)
 		{
-			Oid			opfamily = rel->rd_opfamily[attno_skip - 1];
 			Oid			opcintype = rel->rd_opcintype[attno_skip - 1];
 			Oid			collation = rel->rd_indcollation[attno_skip - 1];
 			Oid			eq_op = skip_eq_ops[attno_skip - 1];
@@ -1646,8 +1645,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 			so->arrayKeys[numArrayKeys].attlen = attr->attlen;
 			so->arrayKeys[numArrayKeys].attbyval = attr->attbyval;
 			so->arrayKeys[numArrayKeys].null_elem = true;	/* for now */
-			so->arrayKeys[numArrayKeys].sksup =
-				PrepareSkipSupportFromOpclass(opfamily, opcintype, reverse);
+			so->arrayKeys[numArrayKeys].sksup = NULL;
 			so->arrayKeys[numArrayKeys].low_compare = NULL; /* for now */
 			so->arrayKeys[numArrayKeys].high_compare = NULL;	/* for now */
 
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index 23bf33f10..86c1cc4a0 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -362,9 +362,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 6
+ERROR:  invalid function number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
-ERROR:  invalid function number 7, must be between 1 and 6
+ERROR:  invalid function number 7, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -508,7 +508,7 @@ ERROR:  ordering equal image functions must not be cross-type
 -- Should fail. Not allowed to have cross-type skip support function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
-ERROR:  btree skip support functions must not be cross-type
+ERROR:  invalid function number 6, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index cf48ae6d0..b1d12585e 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5332,10 +5332,9 @@ Function              | in_range(time without time zone,time without time zone,i
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
- btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
-(6 rows)
+(5 rows)
 
 -- check \dconfig
 set work_mem = 10240;
-- 
2.49.0

#120Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#118)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 5/9/25 17:55, Peter Geoghegan wrote:

On Fri, May 9, 2025 at 10:57 AM Tomas Vondra <tomas@vondra.me> wrote:

I see the regression even with variants that actually match some rows.
For example if I do this:

so that the query matches 100 rows, I get the same behavior.

That's really weird, since the index scans will no longer be cheap.
And yet whatever the overhead is still seems to be plainly visible. I
would expect whatever the underlying problem is to be completely
drowned out once the index scan had to do real work.

Not sure if it matters, but this uses index-only scans, and the pages
are all-visible, so maybe it's not much more expensive.

I wonder if it could be due to the fact that I added a new support
function, support function #6/skip support? That would have increased
the size of things like RelationData.rd_support and
RelationData.rd_supportinfo.

Note that "sizeof(FmgrInfo)" is 48 bytes. Prior to skip scan,
RelationData.rd_supportinfo would have required 48*5=240 bytes. After
skip scan, it would have required 48*6=288 bytes. Maybe 256 bytes is
some kind of critical threshold, someplace?

Not sure, I did multiple tests with different queries, and it'd be a bit
strange if this was the only one affected. Not impossible.

The theory about crossing the 256B threshold is interesting. I've been
thinking about ALLOC_CHUNK_LIMIT = 8KB, which is what's making the
BTScanOpaque expensive. But there's also ALLOC_CHUNK_FRACTION, which is
1/4. So maybe there's a context with maxBlockSize=1kB? But I think most
places use the size macros, and ALLOCSET_SMALL_MAXSIZE is 8KB.

regards

--
Tomas Vondra

#121Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#119)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 5/9/25 18:09, Peter Geoghegan wrote:

On Fri, May 9, 2025 at 11:55 AM Peter Geoghegan <pg@bowt.ie> wrote:

Note that "sizeof(FmgrInfo)" is 48 bytes. Prior to skip scan,
RelationData.rd_supportinfo would have required 48*5=240 bytes. After
skip scan, it would have required 48*6=288 bytes. Maybe 256 bytes is
some kind of critical threshold, someplace?

Can you try it with the attached patch?

The patch disables skip support entirely, in a way that should
eliminate whatever the inherent overhead of adding a sixth support
routine to nbtree was. It does not remove skip scan itself (that
should still work with queries that are actually eligible to use skip
scan, albeit slightly less efficiently with some opclasses).

Tried, doesn't seem to affect the results at all.

--
Tomas Vondra

In reply to: Tomas Vondra (#120)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 9, 2025 at 12:28 PM Tomas Vondra <tomas@vondra.me> wrote:

Not sure if it matters, but this uses index-only scans, and the pages
are all-visible, so maybe it's not much more expensive.

You're still going to have to scan 85 full index pages on your
pgbench_accounts.bid index -- that's pretty expensive, even with an
index-only scan.

Even if there was some kind of really obvious regression in
preprocessing (which seems almost impossible), I'd still expect it to
be drowned out for queries such as these.

Not sure, I did multiple tests with different queries, and it'd be a bit
strange if this was the only one affected. Not impossible.

The only thing that substantially differs between this
pgbench_accounts.bid query and traditional pgbench SELECT queries (on
pgbench_accounts.aid) is 1.) this query is quite a bit more expensive
at execution time, and 2.) that it involves the use of partitioning.

I made sure to test pgbench SELECT quite thoroughly -- that certainly
wasn't regressed. So it seems very likely to have something to do with
partitioning.

--
Peter Geoghegan

In reply to: Tomas Vondra (#121)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 9, 2025 at 12:29 PM Tomas Vondra <tomas@vondra.me> wrote:

Tried, doesn't seem to affect the results at all.

OK, then. I don't think that we're going to figure it out this side of
pgConf.dev. I'm already well behind on talk preparation.

--
Peter Geoghegan

#124Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#122)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 5/9/25 18:36, Peter Geoghegan wrote:

On Fri, May 9, 2025 at 12:28 PM Tomas Vondra <tomas@vondra.me> wrote:

Not sure if it matters, but this uses index-only scans, and the pages
are all-visible, so maybe it's not much more expensive.

You're still going to have to scan 85 full index pages on your
pgbench_accounts.bid index -- that's pretty expensive, even with an
index-only scan.

Not sure I understand. Why would it need to scan 85 index pages? There's
only 100 matching tuples total, spread over the 100 partitions. We'll
need to scan maybe 1 page per partition.

Even if there was some kind of really obvious regression in
preprocessing (which seems almost impossible), I'd still expect it to
be drowned out for queries such as these.

Not sure, I did multiple tests with different queries, and it'd be a bit
strange if this was the only one affected. Not impossible.

The only thing that substantially differs between this
pgbench_accounts.bid query and traditional pgbench SELECT queries (on
pgbench_accounts.aid) is 1.) this query is quite a bit more expensive
at execution time, and 2.) that it involves the use of partitioning.

I made sure to test pgbench SELECT quite thoroughly -- that certainly
wasn't regressed. So it seems very likely to have something to do with
partitioning.

Yeah. This type of query amplifies any overhead in the index scan,
because it does one for every partition. I haven't seen the regression
without the partitioning.

regards

--
Tomas Vondra

In reply to: Tomas Vondra (#124)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 9, 2025 at 1:19 PM Tomas Vondra <tomas@vondra.me> wrote:

Not sure I understand. Why would it need to scan 85 index pages? There's
only 100 matching tuples total, spread over the 100 partitions. We'll
need to scan maybe 1 page per partition.

I was unclear. The thing about 85 leaf pages only applies when
partitioning isn't in use. When it is in use, each individual
partition's index has only one index leaf page. So each individual
index scan is indeed fairly inexpensive, particularly relative to
startup cost/preprocessing cost.

--
Peter Geoghegan

In reply to: Tomas Vondra (#109)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 9, 2025 at 8:58 AM Tomas Vondra <tomas@vondra.me> wrote:

select count(*) from pgbench_accounts where bid = 0

What kind of plan are you getting? Are you sure it's index-only scans?

With 100 partitions, I get a parallel sequential scan when I run
EXPLAIN ANALYZE with this query from psql -- though only with "bid =
1". With your original "bid = 0" query I do get index-only scans.

What ends up happening (when index-only scans are used) is that we
scan only one index leaf page per partition index scanned. The
individual index-only scans don't need to scan too much (even when the
"bid = 1" variant query is forced to use index-only similar scans), so
I guess it's plausible that something like a regression in
preprocessing could be to blame, after all. As I mentioned just now,
these indexes each have only one index leaf page (the thing about 85
leaf pages only applies when partitioning isn't in use).

I find that the execution time for index-only scans with "bid = 0"
with a warm cache are:

Planning Time: 0.720 ms
Serialization: time=0.001 ms output=1kB format=text
Execution Time: 0.311 ms

Whereas the execution times for index-only scans with "bid = 1" are:

Planning Time: 0.713 ms
Serialization: time=0.001 ms output=1kB format=text
Execution Time: 16.491 ms

So you can see why I'd find it so hard to believe that any underlying
regression wouldn't at least be well hidden (by all of the other
overhead) in the case of the "bid = 1" variant query. There's no
reason to expect the absolute number of cycles added by some
hypothetical regression in preprocessing to vary among these two
variants of your count(*) query.

--
Peter Geoghegan

#127Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#126)
2 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 5/9/25 19:30, Peter Geoghegan wrote:

On Fri, May 9, 2025 at 8:58 AM Tomas Vondra <tomas@vondra.me> wrote:

select count(*) from pgbench_accounts where bid = 0

What kind of plan are you getting? Are you sure it's index-only scans?

With 100 partitions, I get a parallel sequential scan when I run
EXPLAIN ANALYZE with this query from psql -- though only with "bid =
1". With your original "bid = 0" query I do get index-only scans.

What ends up happening (when index-only scans are used) is that we
scan only one index leaf page per partition index scanned. The
individual index-only scans don't need to scan too much (even when the
"bid = 1" variant query is forced to use index-only similar scans), so
I guess it's plausible that something like a regression in
preprocessing could be to blame, after all. As I mentioned just now,
these indexes each have only one index leaf page (the thing about 85
leaf pages only applies when partitioning isn't in use).

I find that the execution time for index-only scans with "bid = 0"
with a warm cache are:

Planning Time: 0.720 ms
Serialization: time=0.001 ms output=1kB format=text
Execution Time: 0.311 ms

Whereas the execution times for index-only scans with "bid = 1" are:

Planning Time: 0.713 ms
Serialization: time=0.001 ms output=1kB format=text
Execution Time: 16.491 ms

So you can see why I'd find it so hard to believe that any underlying
regression wouldn't at least be well hidden (by all of the other
overhead) in the case of the "bid = 1" variant query. There's no
reason to expect the absolute number of cycles added by some
hypothetical regression in preprocessing to vary among these two
variants of your count(*) query.

Yes, I'm sure it's doing index only scan - did you update "bid" or did
you leave it as generated by "pgbench -i"?. Because then there's only
one value "1", and it'd make sense to use seqscan. The exact steps I did
for the "bid = 1" case are:

update pgbench_accounts set bid = aid / 100;
vacuum full;
analyze;

and then I get the proper index-only scans, with pretty much the same
behavior as for bid=0.

Also, I did some profiling and the (attached) flamegraphs confirm this.
The "slow" is on master, "fast" is on 3ba2cdaa454. Both very clearly
show IndexOnlyScan callbacks, etc. And the "slow" flamegraph also shows
a lot of time spent in malloc(), unlike the fast one.

(AFAIK the profiles for bid=0 and bid=1 look exactly the same.)

In fact, all of the malloc() calls seem to happen in AllocSetAllocLarge,
which matches the guess that something tripped over allocChunkLimit. Not
sure what, though.

regards

--
Tomas Vondra

Attachments:

perf-fast.svg.gzapplication/gzip; name=perf-fast.svg.gzDownload
perf-slow.svg.gzapplication/gzip; name=perf-slow.svg.gzDownload
�H@hperf-slow.svg�;iw�6���_�����i��(�^�����{m�g��q]��$�)R%)Kj'������Q3�4�m����@�_�f1y�Y���f��F�"HFA�&�FKR������|x�����%���|��W��{M��v�G�u����r��7�6�v���F�iQ�/���ri.]3�&�o�`>���
�m�Im@f���i�bV��5��F�>;5��dZ�h�~W#i���F�����S���SD�����f��� ��$��A@XH��\�Q����}���"��..��8�Q2A�PV�#,�/�8�-(�z��QJQ�&Q1]�0����&��N&m��I�����AA�B
� ����Ne��8�����T�H�*��c����B�������K�Ft��8=�fh�����&�@�C`c���d���
R�?����V��O��"��u�q��h_P�3���}�����U����]'(ns��:��X��$�*�a��:N�3��O��q0����4IW|0�~���3_�s���d�[���"�OO��a�pj�/�I�f4r
��yF���2�+.�<�.�i�4S�]NSPL��f>M�5T%@5���%l�p1,�"����� 	���,�b*xad�]�?����\��
��oi:�]��30��� _q�,/���^c��+�t��D���9�FH�(��q��L ��� C�S�����6�2j;��y�������2'��������'���Da�]��<�"��� \)�����>��"��Q�*f,J�#������O4��R���U��j�E���2��������p1����1�����F�&@��9���x=�� <)I>4�i-�P#���
�O,%rh�*��!����;a�W�O��=�p!�uo=T��*`��[
��"��N�e�MP���~"`R�L/'EJ2���,	�%1R��yh$l����tLt�l�����\���	JCG�h��r����������4����{HCg��������p������Y���
yK�h5�~	���R�M����%P�.�`4��W��7��Q��D�N���4� 9�4���GK��G6I0�tDQ����h�&5�"����� ��h���}�5�c�
���:��K�q���
��<'�u�G�������������z�OY"�� �%�����a�Ga�k��
*�A,v�) ���E��M�n\-T1�li$[ �M�5�Fh�k3�XCz�v��W�����������t{E�`+���Z@�����D S�^���~*X�1�-tV�����*m$8�����=\@��5�54H�h2XIU��7{[���n,�S/��
���]�P@�h��C�	w!�h�\G������� ���y���E�$5@��Lb:������pUbf)D�V���d��q�c�6
g�1G�Kk~@u�<���zI4��A��VP<�&�F����3r��(��o�h���w����j�"�I&	���G���f�E�d�[�
���l���������dx��w�������F��J��(%��R��A��d2~�|U
�F�P�4�����C�4u�eBE���3F�NG+[��u���i2)�W��<l2[������R�F���J#T�h?@t�%" q���`�EC���?�{
�����?��R%<���Y%QIkW�*����A�k��4hbQ�O��������l�%�(`���I�w�a�P�P��n���\kI�7��f
�r::6�eM.�]�#����L����iF�R�(�Q!(��!%��!��:�7�<�U�bo���z@���e��I��!��uR���EC�F���*K�'Qy����Q��VH"W�FP+IAt�`���
Y�����AF�R
�T�BU���xR�P���(9��?J�h���L~sV�x�F�6�*�C�N�����A�d��1%�m`��0����$|Sr*�A�f�-�I�B��GWe��6�il����V�N1g�v��O�*��%�B�o�@zPag�,'A���=�����-���,��(e�jq���`g2�c����`NGN�
��%������6%�������U�-���e��!I�Q,yV��[��B��J���p����[�<���2z�(Z�����/��W?���64�v�.�*�l)+I�9qe/~7�F�b?$�+QG�������
T_���p�%�I�|El�e��/�G��	���)a=�&HA
$g]n7�-�w,A|�b*�	��VC��f�&��-�089,v�hca�0��`�(�[N��Y
�������6q7W��Z����k�l�l�����%4��l�  a2���L�t�-VV�jl{�O����)�F9���n���t��AL ��0�q�Bc�/�9���6S�w����+���Y]8�N�a���]C�������
�J3MmG/�v����I�S|d"y��U��z�aD����Y}�)]+�*Q�����8��
��b� !���D^>�e����^B��x��2��a�<��Mh�w@���z)��;��J��j6��Q`[�~6Y�r��L�r��j0��{V��p|�z���c"f���
�G�C��
�^����J�(��� �!4$-j��qY������	{�U���&���R�~*��m���7�*�	=�=�Z���������
j���f�v5����-�`Uc��Z%�z7��6J ��(��w�|�����K�c�=�D�m��O���s�1�	i;_��q�"3�*��M�2-���]{��.��]O�
���+�3eO�o���;��g��`Wr$�W�nh�O��NP� Yn�l��K���s��V ��!;��nl��X�[������<A��G���q���yre1G9x�dj$r�^a��,��s|<��sf@�
3j������<��1��E�`�����%UIZ���8)�)_���5����B��~�,X�<E|l���"H�9�P�R,���7��7|.���u�W�!��@+N���6~T4��q���tq6����Q.�\sG���> E�j��
�C�M�k�����O����"6�%�'��~:t����S��3���XFt
r7��q��3�h�K"7�Y�$��Fd����o����B�V��H=+�N�?-U�S[���uk�����m�j����4L1�!5���J��M6QWr��\Ia���,S��W�Mz~�@k�x�&�o��N�j�'P8��5���%n0��%���3hi���j$�k��k%���S9r	
m��~N����5	EGBD	E�Pi�����/Q^!��-�������b�(�V���k�[y��I��pr y(@�Pj���*����.6�����o����V��	D��v5����(S8��@�gg$��4.�\,�a�Op]�U�]�����Gu�Vl���X�t`G�	�#�"�*�O"e��:���^��V�������r='A^��H�Bm��$)��^�����%�K(+��8A���(���dx��$�h2���*�{j���J����AE��X}*�+�T
�
:.^w��-��*r4�
ii+eA'��W���2S�#�'���"2	YN���F�2� �/#� �l��]�Eu��Q��j�����l�f�b�������s�;*n$�����3qQ�Xa�xBq�8��_BH:�0^�PF �o/1���\H"������@[�ya�#K���Q@/�,X�U��������#F�i�XB�J��b�?��� 2s���0"�,���o�V��<�k��k��-�����]��DlH��>K!b���d���2�d���'w�J��'SI8 ��� ��h2����(���b.O��~�<��D�q�f#�����g4�'����(�"�I��_�S��9����2%��S���\^m���@t�{�aBu���D�)�~#P�����*���H�/����6���0�"���EdGS</�]Z��z\���se*�Nqi)�	N�
��,9�n�qB�e�/��y��Z~�E�6Z��n�N��@�� ������K~h�������f���~����f��/A���A�#��m����
��F�>�5;�����4w��t4����}
�����4yK'���~���d\\`�C�*L7\�/x<�@nc7�8�N)���s�ml�����E��yKys�M�&ZZ���0_�l�����m%�����l��]��u d�/c�k���-��f�J��t��n���K�1���m��2���g���8��p��l���
����cs��mz��$�4K���:O���dkv��*��K��,%����F��0e����)��1{B�^��{�'D�7�0Sn�g�\���S{Y�]����&wa�|���������1<���a9;�p���ak��~���
���o�`N�0���.�Q������������#��;��E��U���5��&�6�� ���������t���� ~��3>l�y�e�*#�.F��\�&�~eQ�����3l�3:
����]��o0��R�(����tx�\��ot���I����������n����t����3|����^��}�� ��L|����b<�����2	����}��^��:���F��<���C�p�����2�\�}�IP��JC���=��w]P�bL�n��l_��^��?�`H`O�a<�1m��n�k�z �����J%`P�m�:�B|���.wD�RuaP����
����{�*��A�o��p0��V�D@:���a{�5f,w�a�{"m��\htrL���ThCz��H(R����`0��aX��5z;N�1�B�fvw��Cn�|@%M����t��<�K��e��p]�����n�/\��Gd=(Iz
Q�oKo����r�~((b��a�|��n�|Wr��Y��o(�|���q<���&��e�w����D�@I�����R����{J������q�R���k	CmH,���C��6����P3$wW��-[�0�V��~S����m�G�6�4����x9���B����5�G$����~�1�����d��\?�l<`���gv��f�������O��k���[�#�G����AU���>����0+3�A@����Bc2<(l����|�-bc�-����u
�]4$md��>f;�O��� �G���#(�
�����aru_h�&(*��M���l-��k7��&�.��� d�u:F��
��\�w�	A'l����`�$�_���
��W������f�zC���L�'���T�p��q�D������aV"(�W(�s�^��L�F��5����vje�!�������������X���C���:v��ZS����":j�/�&W��'�<���8��t�F�8[@,�:R��5�"������L��N��yZ�o�i�OA|��g���l�nA�-�n�!��<0�#���� �9:?0�t��=���)E����98�����y��-��t
U������n����gY2Z9GW��0z��M]4�}n��� l�������E��6Kg�x����X��F�s���O�<����<@��e�?k����d�����}�e�G\���Fv�����=���$QU8E�T������d&wT�l�����a1�����O�K�N��)��%�3t������~Jy��%m��6*z��g��y���������a����zfz�����F� �F~4�1�
���0]D�6�-K����B�������Cs5��ts��~\���>�GXX,�\b�@��:�5���Z��\�14�g������&
m����]��+�}Z���Bw~?�����������T����X��p��jM]/�/��X~^mp�2�L��Q�W�#�.
_��O�b��(��$����O�B����G�25�VuT��b�^�@���UCu4_���>���z�N��=<H_����3<�O��n�9V��9����T�K,D���i�:��_��_���_���}��i�G�
�"���x��	��A���a�C��q ���k��8L����B�r����i\T?�G�E��>����'LN��������q���F:&�z �5_�c7� s����3���0���enX�a�����T�����U���}����"3����Zv��2=o�zx�u��Q����^�g��pW�71�,ZfXK}�o�n����R�0gLj��T�D�����N6SM���~��Z����������*O2k
[��~�B���m������������t"<���*��cT�l�L���n��e9�~O��
�����-y�J�����N���Jxj��]�_���6�y���� �.|u��lee�	��|'{���%h���QuS�N�M<j��]�DK���,��a�f������{�%k:���q�����%E:��/�4������u���1'A[u�����H����#�<�N
��J���L������}��W��D����y�?���W�m���\83t��K��R��;S&z�|%���9���������tV(���0n����&��N���� <���d����d=C_r�K�s�.d|#�B������Av�N�T:6�|q9:�f��{����9>�������b����)+��wy�d�0�?@�>�����U��/�����_�����������DW|�x�<�=���Rm,�����w^�^������_`�����E,���������fd�-�3�Z�%kl?����_NTA^6����S��sb�BE]��UX�C(��rwI�����[0�W:�]tI�-E��t���T���?x�Y��������>�]�w���/�����4��������$0�����nX��E'�����R��E]P���bM����U���(���n����b��(��8������y@�v���3���K����T���X��)���H&QY�*�1��@/���3��7���1��aV���Lk�
��K:z'���4��9i�m����ty��B���~��:��~������!�p��}l�0�@`d�{2w:��G��=���9�'���E���[�)�.�y�����Z���[��[�t+5�v��x%��@]p��m��Q5��Z�Tm�G��*o�+�������,}X�jX��L(������|�GG�<z*���������:��m�_Au���[�cUJl��2��1b����22(P��|�>������d��BO����WK�K���i������]
���IL/a���Q�a
�����_z�b�cx���2��NA��?�/g**�t�'�y�!��>��#�=i���=��f�E����j��xe�Q-��Z3������<V��T�=��vZ3t��5�4W^M�+��m8����/�Me�����h��
���Z�
%�����eY��!QK��|�euR����������X�q����*t�_���a�j�j�25�w^>U<:�����sH���p�q2~[?����c��'����
�S�1����U<1����M�������1T�;XnV���P�S�Ot���0t�UA�k��!^D�+�(�CS���Ts%��P��4��f�9f*��^�R��Q�I����+�=��X)*�-��
A���M>��M��0��A���9)?p�GP��M��y�����"�K��`�Y��FSx�:�.��h|�<p�F
��r��j
��P����~z#�}#P6-;�4`Fp��Y���>�mHU���/L�Y��p��<�
�u������?��H�����k_SpE�;_G5��n�����i+[HmWHPY���q"&�s�,6�)-��*6���km����t(X�8x8��'1';�D��s��#6��������1����]\��/8�������B��V���x\�JdE����/��D*����U@dp���1������#��m����@��YL��)^�k�oSh���	������B#��������z��q�%UZ+z��?��J��'�.t�f�d�W5r�Dy��iv[:���L�3bg�.fy�w�
��`�m����w�aZ����d<Lq���Q������� ���|T��~�FZW��V�JS� �FW�y�'�
�G<|���W�50�2z%�Gd��ihr3t���u&�T��Q	g���$p�t�OGz�*��:w:�����w�����h�R:�0T��h��Ctq������)h���h��Ca����9�����i��?��O����A\"�V��l\#�|
������a���<�'Z�Ei��2���B���T��c�S]��:��ld��y:�����1��7��� C�Bt��F:��i�T����$`G+�WU���Mt\�� Y��k���q�������)�|�����.�����-�eh���Cio���bx)�f9�5|�&0���������gzhF���i�lB|
�j������1��wP������_��+B�~��c��;��`a�sBM��3C@��M@%� �LaZ��6-��EJ��e��-A�F����P@��Y��F'|�A�$����P����GfI��~�+G�����k|U��$8�����M�N�i-������yE���7l^>�^����f�835����~.���'g�[������6�;;t���k�{E�-6�� �R�TU��lO�Dd�hBP���r��'��H����Q;-�+��9j�:-���'5.�Y�jvi�_�f��m���W�v�N{�_����c�\�O9����2U��[8��f��F�h��/7W�
q8B�u��zX���r��-��j���h�t��02����n/�e���u�e��5IfP�f��	��UX������A�$�F������q��P#B��0.���
i��_��cd�:��qPB$����q�~��;)x����nBTro���r��b����Q8�s5D��.c���7n�[�&#��B���g���'��.�*e��X_�u���/w�I���X����y��w����v�z���s�d�C��x��Q�����gS�m���v=`�>�2
�q��r�@rM�����y>R�����wh��<�5���g�f7�R��/{:$��8�����+��#�wx�N�}d6��M�����nx�Cd1�����-KM����d��bJ���P������a�����{4���(��"�C������2�X�}���t]t����T��H2���{4�%��UJA��;��NP����91�c�"�����?�j���Q#����e��s��7/�=
�%���h�;w4���e�����:w\%��(v���`F�����9�1)���{��09q���<��E>����d��g��; V�g1�eR�����X(au?�wB�"i^\�M�l�FC�.�7����'s
��!\�P8W	�]W]b6b��$�X34�����B����fT�wb3�.�aX^���iA�+��@��?��C}�� ����Ji�	UY�����3J�\ro�~�/VUl�~���L�I�����`�
dC��f|,�\3x��k^x#i^��X>�,��k����5y�
���������)���o���L�TM?��H���+L���/�g�����\�<���t�����C
T����i�iW8��������xyZ������
�5���K��v�F���=�5��lg���~X�`J���!����>\P�|�a�pW��
���d_�g��	o
n;���[�=�yE����\�<�C���nr*�#2y\��(�!���V���
Y�t��3>�L��6w#<�J�{B��B��40���O��%6:CQ���}������M��V	���)U�����!�C�1}\�_N����1?�����p\.��C���z���yMa���raV�2Q�{y��[!B#t�]�+����#!M���J�/s^�	��,��j�8�DW?�Cr�p���fr�D�<%/�Q��G�~>�U���+���8<@	�z�Lx�w27�"e�
���0�������mo��k�����Ub��	lB����8E�V����t8��	�*���*�&b.��S7c�&U�_�����;�Yp�v]�#8-����)�. J���TRE�!(o���l�W�=��+�W�gXH~�<�>�TN��A��&�+j1y���Q��Ze��w������������!���=����{b*�����8�u� �	u������2Og�>X����'r������~�3!/��Ic���b����<?���e)g�O4�u�~�=RN��2�b�"��T���<-r!�Qd���-�/�LD���xa�N���9F���`����K  B�t��� C�Z���(T�[�
7D��B�v�U��0\m?��P����c�24���*�����0M5���W�{N���a.u�H����<���u�Pu��%{!�����f[4c&'Q�gZ^�7���'7VK��P2X��a������>3X���=�CW����0������
g=�����Xl|�{\g�)A��x[w��j����;X���P��B>�����;�m��
����Z6Y�},��<���������P6���r��Ozc����UwP�$2������B���F6�8�%f���%saB\�t�^Sqz*Z0�02���i�.Y]X��kU��V������)��q�p-�a����0p���?�/&��6�GQ�.�������> Z%���|�x=���q���|Q��?U���*
���PW��|��d�f�p��Yxq��_���/x>���q���*u���+��?�O?���Y������71V�;�nC��2t%#+v�%}I�/�e���m�������*���KB�����\�O��#����(N��9V�n`P�J��ds�>|�j���Y��&*l��m.r��zbq���
���_f��7�f2{�����e1��BG}�����CT'7rd��L�Kh�
����YI���:b�D��&������q|<�,,q���}l^���w6�V�X��r�auL��np!���&�5�E��2��!ujx	r�R8I'���(XNM8w�����'�D�vT9l�b�Y�|���"��c?f�����>�z�DGwG�}&�4��j
������qpLE���<,��>t��_�+Q�D�wD*�ea�|������]�O�s�C��	Iwcp�O�Hw��*������:�!�W\��.���?1��45��n�.��$��.��5%�3 9���hN]D�7����'��U/��Uv����RQ����W�_��k��z;�������O��X>d6�v��O��mY�����1�TNq��p�KP;�E$��Vf�����SHVU�"�MA��:a�\n���d�GS���N�"+���<@/����S���R�����������`[H3���SH��VF+oy~9���P ����]L��g��q���<����2"�C�9�F��r"�z���Ac��S�,B�>|E�� \US(Z�����}�t�N�4��m��`u9#����e
��?����	�C����eC(-���ov�:��|�&�~b�z������#����?��0HN����4����W��*L���+w���������>�vz	���=�aG�n������v��.���%���0����7��c�y�[�r���������^>:��u��&�K���c�R�����>��?��?��F��wx*Q����~�e�������2�F���I�
l���4���h��
ih����~�W8�����#��� �MJ�[�7�v����K����%�G��0�-�K(�/!��<E(�R�����l���y�z�<}YR���������%	R�����Z4�,hn�+�j�s��������%�]���Q��6j�9��l��A�C��@��U���I���T�������]��l��.�3��*�H��s�Y�
���'",u��P��Z�E��������b�%Y��N�a���%�@�T�6v��:yU�(��M������Vxg�Zd5�8^ �#����42���w��@7N],�|hL�%!7��f��{*��b<����8&��!Ai�[����=�5�%p� jI��m|���{7�����$���T�k���.�����v�����=t�J���!��*t���kk�&(�@�������[?�r���O0{��?�g�Li5h��t�����:���4
n_�CW�n���L���?3���;�vQ���R^�}������<�}�]��s�����|U������UM_��K���YxK�uD�������Q�9m+�fky�,��w?���}��	��{'h�����;�oC)i(�	�b6h�oo���������>;-!�u�Zf"}d�J�=�m,���XK(��baM��y6Rc�^�3�8w�]�E>A���\Z�
��<;-&[X]_��?_�}���y-
�9I�I�yi��`�k��Nsb�@<���s�q�{��Z�Nt^��������n��/Lx�U��G,V���2C
\���1����) �
��H��Q���-X�i��c�6���]�]��1 �:]�s�]�}���%����Pk��Uf+�)x^	��b������
�Wn'��)V)�����@���`�_ry����_�/e�;�������]���y�Oe��d��>��jew2��6����<B�r��oZ�n����J��jN�c��]�e���3����=���f�\�!T�����G����`S���X���;�&�V�~�]�B�Em��x	��dS�����9�B�Q�!��������@�����f'�>V��$�	�P�����<@�	���Ht�?l{������_	~��V-�s`Nw���f'+�b���v���a�,���0���y#������e�M'k�8�� ��:kw0�27�Ht�|q09}k�6�k�U��E�)X���{��x�����_�������R�@}�{���Y���3�R�jd��\����-��d�k����e�)��2>�2X�a��If���8Z����>�����������Q���P{�^F��C5W /�r_5�|=�<�g���.=���bbi�7�<����0�W����
-;S3l{%������T"h��d#�+_����&���N�3!��K�c����#���]��|e�Z(EsVT����[8��a��^aiQ�7�F�����R�������
��/�B�g����{9<���y�%����
�n�PC.0�5�d����w�a�\C�������U�������7���:�1Q�����(����S��?,��x��~������I�;s�� �����8�������e?�a�{���c�-����F���l/_�+���`�����xZ>����U�_6���ayX�w�O�$<o���{'�����F�`O������,�E?����'�]P�]@ +��D5Se�0X�O�?��;js?�l?�[��������gMj��D���@�Q�X���D�����pW�44�y�f�Y�U����<����oT$x�����R������E\m^�+�VsK#���Bz/!�6u(j/u�N����\�MB�sbh���?a!��~��?�ww��*j���oa[E�K y��Aa��lN���HfQ�+G��J�����������9�v����y�XX���=��_�90t%��V��l��D��~���$���9��	f�E�I��)+���|#+z9n������1<�
���g� c�Gb�2�%�X1K>X$��*1_����5�+A�=������f�0�9��8�x�k����������<�]�Xp�j;����@cc���Z�������z`t�+�B�E�������Wh	��������YV��y�M�L���5.hv����t5�KU3�"���O�������a������z�CJ��r5�����mr�0�
#T^T�=
�1�9]��7�J��t��C�EP�m#��Q��"��m����b�n���������M�W�����w&�Q��U��fq�����3*S:@�t���S��dPQ���8M,�z p]}S�w�.dl��~uZ=�>|���w�������������1���VGU�UT=r�E"�b���!�lO+,d���n�|\C�N�-�g�D�l��~������?�3�T�^m�RwF��S��FL��g�u:F�.n�}-����o�����o���M�<dF����[�;���n�jF��a�>S�c�g�|FjY�^Nth��%jZ�����6
*�WW�����
8B������:.?���8C<�������AH�:4q�����GH�!�JK��md�D��.Y�.-�47E!����
t�S4�Ex����S�����3��c%���H�H���5u��{*�4������'��5p���KQ��:��2��i`�i{�"n^q�K�{���p�/~���2*3�,������#�����������M\8�&I��N���ll���SA4U	�x78"��0�a����f�������F��~�-Th�Nu����@y���J/7����n��Y?�*3	����1U��j4��U����4#�+4U��/�ir�O;z
�c�m�Zs%}�U���O	�g49�a��e���Y�z�>O$���`+���H���*���}Gk:�=��1�=B�Z����t����ncp��IaW'��L4 ��c�\�!��x��
�b��v�bk%��F��P�d=J��F�ZGwC�����k��o!6���>�{��JgG���jm)�z�h��zmm,g�-��3
��E��D���+'!I�*���4pu8���j�E��v����c2��������Q� ��wL�������eD�2b��|�Yz� i���J���'����B/���1<��K}�����.1�����Ta����$zj��8���y�d�|�P��������������'�n�����6�j������@-{%������?}��� ��[�s2gs'�F�����i����I�9C�a��W�g�;�6��2P��g����4��O9�f����x'��������-�5���an|}�zZ?����L���m&�e�G���$Hn���v}P��D�N�q)UW)�������f\@(U7�����@�Y�L@���!�9����*��
+���=�����B���,�zn$e�]�+\��0g�c�L�@?;�)!��A�FOs�*�nL���Mo���G���Ftd���nL��E��i[��r�g2���`G���T���w-��n��=NW���2��+�Z
���,7C�r8)�g���-�'�@�\���,�/J����g�s[�_�T���~�����#,3�@�`T�g�)s��O4����_5�u?���R���M�;���<1n�����[�����jOnUwsH�0^�B(lg�����y��o��"�Oz�-����L�C0'���	�t]� �a�;i��;y�XW P��2�?�Z���&�B�~h��[��`���SO�T:.Bg/���)��n������D���s1��'q~�+L#�R��7H_��
����P�fB(���:�]�E�a�I���+K���������3�M���*�� P�4�������RUC���O�l�^�~��
8*�6{�	<:��\��@0g�
�.1���@��d0A�G���|L��s�������@R��.k���O�T��:������e�
z9e�/��uE�kU	n�3�,1�7�xM�.�HYec�`-�?���&��z�Uo�k�/��)�6�%�)D�3���7{��c�A�g�d�7���O���z��S>��D5���Rp�(�1��86����6�v�1���`8N8����x��~k���
B(t��8�:%&W��f?������
���X��=���A`E?��l�x���~�������&���%(�q+("#B����'���=W'\��Y���G�x��8K�)9�����.�����@���G���t7)��n!M���\yz�]EcF&�@����{����C�u7}�R��/�p�R���#,�K��I�w����-�92.�.~��g^��>�@!7�m����k�@�b��I� H�"+	�n�A���Z�Oa�mp�x�9,�� ����',�����!Q�0�y�w3�N��{+�"�9?���w[��FZ-��h�S�oh������Z����p�<��Y��Z��mVO���/h�������n��bg�xNa[x9�u|�_�_�J��d�Do�9�lyU,0g���:���L�;'D��[[Z��!1�f]>����r��Z\�'hR�������!�t���D
GIbr��F@|,q���a�w�P2Xd���I7x�=�B`;��������z��:x�D`3����T2[�q��b�:����!�H��gQx�t���N@���q����%n<M��{�iX;�W;�����
{hz�<9%�U��F�n��@3����������{h�wQ�ey<o=(E�|
WF.y����Cgb7>w�^���n��t&��j������)o�1Mz\��}�2������d����s��2@5��d��pAUo�*H�,���GU?w��k)�)x~�h��r[
�,�	�7��3)��@���<p�U�rr�����r��u#�q��O�>��� 0�;�20�q��?O�;��B��K�$���!n�SK�D�!�K|gG�������5�r`A��]���7����,��u����#X���~Y�F��W�X���t�&����TI�B��3 ���
�t������8H��*��f�m�j��A�
�*�������iux����k���&���F@XR��6���M�4����TD?g|��qa*|����>B'���b��}^���<������cq����������X���_`V�3�TI}��ms.�gGb��g����[�~^}Z/����z���&w�2�:usO�0�;�G?��u������B���}�g����F��'�i���F��Q�t��PW�	��
T���g���jO���9�+�lV�y�R���t��9��f�4�n�*bn����1d�`0�d�,
��z�	�y&�^�`��.U�S��;����B�SJ2��j[���}b������:)�&k�7�V��]b�U�J��^���.�7��3�8�q�b&��{���������\n�$[>�Q"�F���������a�����MP���&�C`.1D�4w�����8������O������AP�b���#�*�������g������XU�my���o��������Hw���i�1s.k�%T��3���LP��+�9�>~�l�j���0 �������x����D�6n\�G��D����������xQ�rj����<i���J�1k&9_�����M1�$�9��ffJ�?e��[��a������a��ZgU���h���Y'-G�[JB�`���l1}���ucto+������"��<�\�;�6VQ�	�����n];]�jS��-��������j�_��]��ss�q�w�|��q�6����J
dIw����Z@���������������������a�1��UEP:V"�,�B���*��AU#��(c^G����X5�/a���ee������F-:x�G>M�s�}%���c�x���7<	R����{���o+mId���P��\d�B��5�8�RV�/�����k��]�-i����� f�Bw.����\�*�@��p���>�Z��_�)����Yhd�Y}�]���@B�`�32�w�����Y�[��uSS�&�x\7F5q=#�{�|h�������	7pad�s���������������LO��,R���y��i������cq���9���zG�z�x#���oAz
��}	}���6�_�&�O:�p�����B0���|�.���~0�O�������������x�|T����z�4t'nJ���m�< ���N����&��[�LMB������2�����$�����������_���/�m6C�������L=���}��%�������a�O�>���,	����#@]k�V��N��C��k������6�������+f#�2���R/���'�R�f�].��|0{j��o����3�u��������?Mo
"b��j�7 �5�Umsb�J	V���
�cV_��%����bk=�b����/��N��;��q������3���U����8��8JP�(a��+*����CO�����!��������X�n��B�������!�vuX�L�\��V�C�����d��B;5�F�"����4���_��Dra]���By>?�����E�`�FD�u /���?�\r��z3G�q/�m;��j^%�mL��hp�{�6�������>�t��=��y>
^Q-n�����G!e9�����M��l?mw����}���r����i�fNU�@a����t�-'����Mnq���~
�P�r&�N�*wX�������������?��,�[tg���40*���4�l�hY�GK�c�@[�LU/�����z��+��t�y�������B'r�jn��k������?Uei�%X�bc,����t&���:��#�|��N�s��a�P?v:r
��3������������*���p�|B�[�!U���N�%�Vz�����Ks�X1�H�l���8��lg�9���P�5�B���
:���YS�`,���v�Vw�G���6a�^������7p[�$\Icw8P��+��T��W�������
�>\��7KN�K�3\\��%������^^�
�f��]�@�������c���F�^���S�R��n��,��:%g�`�'8��=H��B�%�U(!�!�g�
��{��.ga*���	��9[��������#>�v2N��V��������M��&j����lD��Ui�8���������E�xx��D��4�Is�t�3����!:U����0�c&cY#����RH��l�������q���[���W�SZ�<�X����l��.�����9����V�.�P���`s}c��a��R��g'����@�_���\��&��+�<���S�e���5[�����"V\��uc��F~����mA�?(�ku���gK���A�O����i���&U��1� ������(�e���]������:��j�,��<#�����X\�&�&���(�\�b�A��$T�d^�0�*.��p�'��k}�G�j�+��
B�V�����J]tu�S��[����b"(����==Q_�X��
oW���/��']���nc�a�����������~��'��s�RW��$��l�Xx��!�g�T��
���"��Y+�3|���Y�$TqI������n�@��q��r��;������i+�9AN)�x��1�E�}��jI�b��H6����A�SJ�<�
$gy���������c���i.c���_!3�/��w�������1�B�cksEl�f,���)�8}{�	����yp@{���N�zic��gOB��bt��3]2�t��������y�b�?�g8m]I*'�q�s�������2?~v%�RE ��|�G��9�����t�HZH�\<���n������6���s�G��$����>h883�W�TgX��m�`c��7U�L�p�]��.i��/�B�b<e:o����0^�3Fw�3���������e��9R���
*�����r�u�3����{�N����M�U�C��5�v��B`o��N�=	��Q8�-���O����;�����=P�}y���XdU���^D�7��Yt�rs>Zu���l�kX~���B����Q����@���{���tZf�)��m� c�a��
��6w�}���x&������������x�����
��M�O�t(��K�[&�V�>�Q{y��H9����h2'�A���[��gq��,a���/:����U�.�^�V�"�@x�������+vW��Jy�3��6�����'���J	�T#�8)���VO���`��oj��������25=��������e�����j�� ��V��s<�m�,v@y*�>.{����%�9v`X��(����u����D�Rq>V�����a�����B�9�����eK_��~�����������co��P�/�l,��2R
�'U����]����4f>}W�/�DQM�m����O��9�XP��P?j#��$	��T����H![�����^�L����|��M����XV�gh� ��M/Bz���:6�u�;�����`�� �9���#��1IG�q��yC�c�a�|�Kt��h�?tC�����H�n��� A���A�_�s���+7��.:�@o��/������.UW��I<:�o�vA�����s3���g�����J�3V��M��d�l��L-f�Jw�gQC����Rl<��2�2�@]]t���(������{zd/�`����N����d�eU��^6O�C�1��=�.��A���<P���h��c��6P�|������Q`���4��m�����+�"E�TYG�*e-P	[\�;�����5M�k	g-C'y3�b�y\nKP�L��n��*RiRf�t�� ���n�����9+U�Vz�098e�r��0{��P/�q�����3�7E�8��M��0M��q�2���u&#$
]��2��[lY������Xu�q�'[��S�sN������A7����o��z!��g��(���	�h	 R�6�������C����>1|��A����]�x��7�Pl&S���}xe����xI
Ued����Z���:�����I1�u���7m�&T���.��L��t~]���tz���a���)oj<�����H������o���M�}�u������!m���r�y��k8���N{s��X�=��H�����l7���?W�H�H^�FLV@��	������d����"�}"��g���A�`��]�)�������� �&P%�.W�:Y��]`���<�w�%�C:
�1��:�[�������<P�@w��/Sa�(����z�b(�<�l{����a��
�0_�5?O����vk�"��?���E����b&����=8s������a	,5�a�R�|��R��U4� �<�d��u����s��qw���`Y�#�[��V�P�������"9D�|����Y7,����cD��
�Z���
��`2�3��*�2|�e���
,-\'��U��s��p��Jy���X���Y����2��s{���}'�>�Rp���kvk��<J��c��$5�6&Ln&�Z��O*������?�1����}�b���\�}q����'`h�C`*<6��l�-rgv��H�g?�i�w��(�cFI��7����_��zzYg�D�[���6���o���p�����J �������L�`�
���y��=`Q�����e��s�Ftyv6g;���5}En��=���P������
:�5V����!i�EA�H�K���5�S���
�??�g�eQ��u�2� �,�0��<�����z���r��|�3��+�"�f��~�����,#O���-�f�X�u);�ej���> 0���'�j��A���LT������^�[�����G��r2�R��^H8�q��	Us�+��6� �Q��p�1�o$���U5�>�����z�|#BA�dWkVa����Bhg�:��R#��M���JhO�G��4q2���"8~�iH���V����+���������|�\��� ��@xl*.��9�X3|��^:���6�d-Ol��r���P���|��R�
q���N��R��7�s����$��B����.LV����,��v�Q��e�`��)��{��VC'c�7W�;������4aJ��V�W6���~N����@�
���gA.�/$L��"�P ��i'H�0i���� �����wBO�l��dx����S�a�<D�@�N��V<�S=!��Kj��i$�����4���0�~�����ek�^	"�w��>V���[���8���s_[���B<�� }�,!��
�{k�,Q'�&��4/�}����m����v+[���N����
8�F�v4�v��������Px�d�3�h���]��g�������,Xc�����]��`JD�o��@A)`��?�V����q�M�]�1b6&R�3�WWs��j|�R],�q~����[��rW
0�*;#�T-����.WEA��:�y�#;Q�I ��N�A%�MZ)���W������T
�gW��]��������M
�u<'�5[���i��M{\�v���z��R`f�y�� ��U��-���1sGv��X�;��m�z�%wN��2��,��f�:�G���9!������Y=Q)�}�c/]_���:[&�Y[����0�,�0 ����K���2H�jN\*���A��A�}U��V{8m����t�;�#u��	3]����MM���I].=:8���ev ���T������N�tM�"uH�A�^��������
.}NgQ���V���2��if-R��u`W�5����a�.G�z+t,:�|[�S�b���VTf�mD��\}�P���	u��V4)��(@�����#E7���E�������[$?���=:�K���*��
�ASJyU���5���g�����v�/�����U-`��98&�{�7%~���@�G��Z�H���nU=/�kk��ZX���@Nz�cd���c���0���b,rS�(��D����
��������@MyXX+H�F���*�[Up�{���6���O?/��z��)��j�� �#������Sk�����O�	�c�B��������������Mh���L�
s�7�~S&���������p��8���7��H�����I�	���e�����f���2�X���j&�}T������2� ����|�)�/�	������l��*qJ�k3��
�
O}�9������������N�i�\����#r����K�����f��d�,����X�G��e��c��;|�!�"����}����:S>�,��R�Y-;����i��y2X�/����u���P_*�=a��
b^��,������.��4��Bo����>o7Tg/l��h����8�7�{�����!�&�e�^S`k�����/�53�+ �}�C�#w`�XU�����x.\��I�v�A�d��@�K�u�O�1g���/O#��2������9A*�8��)����%#���g�w}��d-]i��*�����.�=��������u��Q��m�@���-���.��6���(��t����B/�������!g�S�@}����j�J�*���-�:�4�q���������L����<����)o_�)Xt��wu�|Y�{�%�yDw~���{�r��pXZ�������l�pq9����P6K<j�����h!>��|����{����gna��u�V�����!��B�
G�iD��}�y����0J�"���4|[V���;Z<dYg@\��v�~)�c��N�<�t�JV�]��w�X*�H�x����A?������>G��G�T�Q��|�i��v�����c������]-6��d�c��Ic�>����}O���
A�� m�nj�������l������^'W $�������������u�>P��pn�}����K�p�����
f%GMd�7����u�l|�����"*�;n����7����R���]�����Ag���|u��_�����	��X��Q��-1����B�H���y���^WhFff��C���?`y!��=�Y���u5O�Y�d��w��!� �Ml���ep�3�{�UV�^�P�t�w��3#��l)@�,��K'�3PsE�M�]����,WUS�h�+'��j�P�u�~s�Qw�uN���C�	�����LM��cW��Ix���2b���1�����s6MK�*<B����:��x����f}:y�Q��U(<�pw"����������M!��n~hy9����=���2����,����N&�ys���n�Q��[��<>c���Q�!&�p�������n���WO��gW������H��_p{�<?���u�W����������B��"�FE��X�.U54�����H[@���L����>�X��qE]/)_�|��=������X��C��Eg�A��M
���������oZ���� 5���J�_��;?�^��0����Y-Q���\����Ia���H�����X(��{/w�h�\H`3�_�|BHy��s��1]�����cnD�win�p}s�9�������y}���{�m�'��w���xX�u�+u� '��'����Y�sS��-a�mp�)��l�s)�����������j��|��i�s+�������]��Q�0�����J��i����u���C]g��L���������a��>�=fu��h8��:���9��X�Gd+�WS�;�-��WO���z�]�3�C�bm��d�3f��_Hk���_�v�Y�RFa������O��7�7
����� �j"���Y��
���.;�!]��$E�����~2
?qnM��'���w0|a�	��s�����y'��@�*���f�	lS�e #���k�D��?��{j�����t�C��7���R��UP-@�bND2bF��t7
m	v����P�aB7ucD(����3|�<����5��K�u!\��/�-�����k~8�}��\��M���X��DqW�n����	�"�;��%�l�P�f]=����#�����^�/_,zO��O/��_m02�������`���qa����Q��v5U���j��aZu!�w��/�,`�Xo{>n�l����� ~.��-�x�"���w��U^�F������h�tZ���N3�8iHw����?���%)t��BD?Y�1�f�X�V	|��++Xf-�sJ�B�+@b9s�R�UK����:'#,�����z������4��{�H�2������Wqa�;�
���������wi�y�	?�����GK��%�����k�Jd"�N$g�.�Ou�����I�^_���z��F�^���T�>\��9���N60����0�9���#���������_�L����*�����r���R�����^��R�3���w7H%��!�cug��&�������b�V��X!���/Z��K���*�"��(�JhUW��p������;�uo+_����6�V� Xs~���v�4�/���B���;�I#����q�`����T��M��S�C1.��u�(4����dn������,�h*���^M}�x�����E�p���w�����m^�+4:A�n�\�.�B�eu��4�7��������������b���Yh�N$t�\f��6�% Q'�����7n\^�=r���Qu�����r��Zu4#�z
v��r����\����]���wy|�9O=�����?�9�X
�v�����&����e��Z��|T���]��I��������SU4����V������-����
�VOW���a7QUUz�a�8a���i:��rf��~.��N���f���-�C_&�y!��>��B�7mwFi���hF�qFh��y�n�M{�,�U�j
@�?3u�z�m*���~B�JZnU�SJ��m��8
����i��~�<�)������*Fk�.w�.���-;�*�J!��/�����T��m#i�t����-_��\�<k���&T�<�#.��+���<5�����5��w��>%�bBA�������		�}2!w br:���3,cf#���r����r'��V������p�[�q�������&����g�H�}���)��j4o4cm1go��A��p��������c�������'��b�4u����8�.���T�ze�C�kXe6	F.�GbW���������)�w&m��L������{�v G��q��S����0�uE�nc�� +�F�3 ���j��E���T�D�>�R���YvP����+���Zt|�U���o��]Dz�Zh������S����Ew�6X�U%����@��k���&�h�����?����<��Tlo����|��P������ #�>o>�����;}S�C�~:A���Z�W��9@a;�X��RZ��}���0�:&���K���B�mXWzM�1-W!V�7�� �Y�h
-uQ?�0������m�Qz����{�<=�{Xm��L������[wokU\S�]���Y���KD:��l>��X�V��������������f��u$�?����1���9�m��}����H�<��X��&���~9�l'y�p��zX�./2
���;wfNX_?�O�b Gh���.0�'F��!h>z���
J����s��\�b8?'j�E'��8�5�3,����'D>����*��	�	�3�QP(���G��}7�M�D�:��yn�K'a��k��2Y)�6,f�t����!��X�;����J0a�~����2#��h{�A4�[����ry�U�S�Eq�:{�E^Ho���L�e(�X)��l�w!�]&�����]��4�����;�����������j^����6�9Bo��������3@Oj�|���U��b������U0C~�tF��V�F��`u�N\��8�p�
v������s����*�N�7����<��ZV�J?���m����g{}x>f����3��0����-x
�
�Nt�;y7���|�f�����;�Q��7���/���HU����a�g-4�Lh\u�AK�e���F�+�Z�����I�6�N�����e����Qv��a��%>j���k��g�?>���	�AS"�Wj�~��H������Y`ra�`dg����K��`o��<m��I�
G�/:7����D[(z3����N���d�J>.m�\��Q���a�Ta�}��]F����o�=�#���^,^-�fXs�m�{�<!����1a&��#u�����	q��O+�g^���1CW�����j������&���3%�+��r�t�k����y�����������q�yG�^��2u��O��������y<Yb��#���"���?}*L����	r�U�^D�G3{kn,�ex�|'�������b�7���-g��
�4���|�x[2�d���U
�G`e�BW�����cf	������STog���J���Pu���qjW,����R���UQHgB�����Wp�����s�\����t�u������w���/Ko���x����V��?�����������������6�u����F�WA�XU�Jo��g,QI�2����*�%��p��� �v�G�R���k�h6G������
�h�XCW�iQ+��1'�ayJ�I������^Jsw��Jt��!����:�$�lH ��������n��	�+C��*�_>�'���������u����'3�P�*r��<gy�$Fa���2���Eg����5[���*1�r�[�w����^Y��~A2�+���d
J�����v]�
gd��V�F���>oI� �P�j����5%�������y����A�dI�f*Eu��#;w���g�j�1�g��P�m=�t����[(���d6�7�h��-��U� �{a�A\]�G�qz;X\�z'w��u�P7t���{s������]�%�����m��D#P77x�R�],6�����t%[���=VC��z�n�\>�pxxy8-����70����o�z8�%4�Ct�
�
�\$�"�����6�^vs�X�Q��s[q�&V�4l��-��O�?B��W��F^:����C��i�����7/�;�&�7�8���lR/������JB�;=
��a���*W������FRX?v��;��~�j�+=�O����5t���qW������E���-:%k�� ;��u�����X���w
�6\@�tc9+���/���!X*��c~2j�������L��������#L���sIy���b]�,FX�Q�W��1cP�Wl]��m�jjK��bSj��w��K6"�����.~�r�%�����,4����u�����0�U�	��-�R�.�*����@f��	���_
8�JF��i�p�	dT��c�6�����ru8��������<��Fq��zS�f�e�
��!X������ �srt��M=g���0bL~t��i�X@F��Y!�q0wu��	���=6����^p@�(������q��7U�:����qm��;�S���N����E���2W8��8���;#j��#���CO�������X5�cv�t�m�G�:���]���g��+V�@��=Uj�P��M&_��3�A���N���;7���g�]���[�K��L�E�h��>��g�_�jc�pT3��#U��]�*P;�%)�h�	^�z�lOF���B��)�s��W]��YWQ�2m��X��L��[R�x���"u�F��B�y�c7���CS�1��w�1�a�XK�I�=�$�2c4�=Qc��FBb��^�CV*��6W�����]�����4g���+�F�>_�]w�*�J�{���#4�����}V��QP���������_�%j� ~�[��d��\_�[�j�������6 G�.�iB��:�~]�>��6�S���>����n��ZI��gX�#�tZ�I�����
�l�c���a�`�9������.��18m�|�GH���o����v��9\��w�����|%�H���E���r5
���osin*�Q*{�EsK���hk.��j/�����I��?��}��c��n03~5S������X��N�m������:�����	��3{����Uq'+���1/�x�����Q��j������\���$;����v|Z�/���i�N>�v��X"��s�8l���1Z����;���aW�>�4l���B`���>�40�5Ur3�~T}�@R���M�Z����.1��7�8����q���=��8lv���I�$Y���8X�w�"k{z�[R������aj�$Q����lk��6�jn#{��G���5���<f
(k���S��������@�10(��"����clq�G��x�
�Z�M��z{�y�4$(:^�iC���c��Q��<c���j,j�
I���]5����
7h4�UsE�p6��-\�#j�������lQ������W/����5��W ��|
�f�����0����(��yh�����\�������?���>�D%Q/gX�����g�.�7
�����yD_����9�bB�|G�a���z�A�N�Z��A�32��~x�6���m�?o7�����@��F�%5)s*��*������M�`:���y�SG=���/;��/��w?d�������!�2����������y�ei������|������?��+�����J�I�����P��T(L1s]7���������W���%v�$�J������cd���o$t�<�g���<������5
y�^�������e�M�aR.-\�7[�Z��M/���.��'�W�d +������/��S_���D?����8��M��i�6�������:��8���g�Ma�nh;_d�Yf��~x:���~���:[����<��;P]��:7�/:��P`��}';�,���*��^�G��m���6w�>�7�y��}�Q}f���.�Z��9=[K|�r\�n��>��.B�^�0����6����uum�������}V�9+��m��
�t�?����
�:�,N;��PWR.K���j�jS����[6�R��^,�u%�]eB�7���z�Ua)N#���ta��Ov&�,���K0C����W�����j�������'[��4�/�� ������%��r"�G�0��]��%���9!�>���6xu{x�Tg@Z�������?U>L�I��$|[!�s���4��/�s��A��~����
?P����pE�����'_P��ER0C~�����=n&���n��-��s�����LNg�����_�i6�z��������t�~Wm�i�M3���K���u�-L+��L	�CCe��<�E��R)��0��4`0	�X�Pr	�v]������r�k2OS�?�,N
��vN�u3�j�FQ6����]LM�^���)�PW�e-���WG xDs����" �_�~��>���>���#f��~��D��a������)c$p�$�Q�/����3��x���i���&����4H<���DP%�j>"5mU��p&��q�)K����Y=-�v�O/�X�
�i-veW.������c�G�0B���z9�,���������9����j������������<x���=��N�M�����G��s��Ns�!����mg���o]�,1�����o�D2�������`�Cpv��&��T����[B�x�����j�i��:��3cm@�������[Dh�8FM��������j{\=�69�������i:�����O�&4�nZ
I���[���7��f�s3=��n�qX.R�i����5�JD�YX7�`����Y��e!��i����W�O]WL��U������Y�Sc�[�����Y|�s)�)��
���:Y
W�~D'@�.���3_'������>����7�H�>���������/��+4Q�d�.�n��El(�������5�h�����k���i9�;������LgZ����lQ�4Cv��^b�b�H��T�R�D~m���Zhn����] 9b���)
w��.�^'[������v}Q���($�]������  V�.U�M�������8�lN���{}�:�+l����kv�\�r�W����3B�y	���{[Q�/8��N���y�������)�y#B�&�Z�~uZ=�>|������i�t.�w,�����eYu����h4�[8�k�d���:FXz>&0+z.u������pY�0��*��>��[�N ����~�BV.�����:{f��i�����	�
^�GJ�7?��v_���2�����U���	��k�"������)	��������v���O��J�n*V��;vb�7�M�#h��a&R� � �A�7Xg�Jk���l4�	>�g��*
���>���$���''4�*���������WT|��_Q��B$�����u������+1��/9�����"�T����t5�@a%��{��"R����<@C*$���;P�������	��Wg_�i>� ����?<��
��Y��-�\
_�JF�U�A�6�������4|@��f��^�;��j�vx��f����/���~����Q�W}�	5�e�����fyzM� ��+�+K�9��Xx�A����t�V��J�u{�-M#����#�����z&���_��
��A�!
7�Y�$;��y ��U������>����s�-��%�M&�=���,����1��U
�L9�BK�l������("�0�7���UF>�4S�ue����?�gEf=WX�5����.z��
N-��P��L}q�Z��.����������?�����X���)����!�\����O��c^�W��R-�fS��nN'��Rs6�g,u�gn~w:����_�K��q�d���u^g�o�L������6�]yj4�P�z���� �gV��p��
�rT�4��G�4����>{�XoE���������TX/l�f���O�%�(��
$�~D8�x��>���xZU������FW[�l�Rnv��O(�4���c��E_�]�A�����%U��z�X�`%5:������enE25�u�=�O�����t6��l��.�*��w���P���%�E�p�x&��ku�i]��
I�u���u�.��n�i����LP��m+d�V����qn�
;si�&���9;�����W�f��������U;�*�.\�����:�l���jg�/�Z �9Q���r#�9j�v��W�pq��	�#j�a���NWD���:�H`!���������FsCu�<����\�}MAB.����	��4���+����\������d�3�\�M�P��S���%��L<X����	�����V\�M�@��f�
P2��$s��j�t���;�����_��M����Kp(|�-�/'������P�rub���]��K�
�^N\��������@wvy��	�&���hn��DR��B7[���3�Z.�,yY4�3Q� ����=����^�g�'�B�g�n�?UM�G1��}b=�����h����{��6��o�Q>%�#8JK���\m����m��	��_�*���UL��2�tU4lh����4`�J?a��^eu��h;z����Lc�;*�@�5���s������l�Nw����myr"������7�~�U���M,��%����}O��n�����/���z��R������w��TV�+a2f `�
�[l	,a�4^���U��O��@�@�o�{C����1�)�\w�%����W�����sW�j�_*�SkDX������gM�/���w��Z
4c��T�����z���Z��8��w_v�#��5������Kz5��lR���������f�BP�~v*9��2�>�������+:	U9"��.?}Q�\�= �ak�����?7~8)� ������X./H�},�.���e;^�l��\��^L$
v��X������� �v������\Ye��|H�Y��o��������soF���Q�
qe���4�h���g�_;0L���2EJ_���:v���d&�Q�#�����Y�7��t�P�?�2��%.C�,�'a
wQ���}8���HAx�>���������'e�	,6B�
	�j�#������������9}s��������w��`h��6F�/s�J!���������X��tK�2s�j^�B���C��.���{�R��XI��/w{�!�MF��Ws�������d�|B7.��*�V�+d(�T=K��v*�	+ q�qt��8�j4k{��=�F��5
��w���<��%����ru:6�/�L.P�
Q��G�>O�G�4	�t
k.5�Jm�bJ"I/E�+�8��U�	f\A�MM�L��/��,�b��/[:����rE]#�����T�g��b�k�����/�.{�������)k�~z�<�w��)Z���9�U,�����k>M�g�k��S����6�(�i�	D�s�?������O��/���`�"��3�m@��'7��l;�Z5��Su�Wc4�{����po����/`
�&)�xe678>�/sF�t.io�l���sU����Lwm����w}��j��oW@�<�0j��p2� T`�h�����g�Kx�z�f�Irm�7X�������;)P�����S�L@��/���LZ��bU����E������A#I��:��@o<YA?�U����Y�q�wI?;��r��=��}��>�����9�ZO�*|��/[����MXa��TCt���[�s}I��V~��Q9p�X���T�
�>W�|��h}���W�����2�8��,b��5�P�]O���o!er�8Xo3E�Btd��*u%%O�����3-3gW���vc���^�|{[�G`�yY���a����u��|XO�C�9:|�<���N2�/���A��J��h�a�F����g�w�����G FM�_��bV���V�E�$
RcB�D�c����S�C��s��JZ}�E���*����-������W�P}�
��I���<�I*�
�����p�#>0�s��`���g�<�F0�</���������0�7�4F�/�{��'��/e�������!�{������mdW����B����NQ��Qx���)�s!�Ety����*�C'�a����An`#]"/.����������^���,r���)&���/Sbo�K�
{wA�'���R�
0()�l`��Q�|�����������f���c`��:w}G&]VzQ�����M��iA�2�w�+k����L�����D#��Oq�S���t������o
���&�6Qg����M������,WlR�K�>L�[[E��a�g~��H�9�ghb�jd|#�\����b5��pf����UB���(�$]c�6��2�U�j�y�n&1,�4>�P�|�����}ft��&ma]`����VG�(�_�������?�O�������y{�XXt�nf��O�:
}���y�<7
�����-j5��9���m�3q�7_~L���"~:�����kc�6��F�i�B��!k����D�1	�q�`�T��B��_L���������sI]����?��j>�lEt,;W�}P"	{q"����M�[|P��f�7�� �>Ii�R��D��%Mt%���%�@�^��o�yT%�q������?$�ah�io��5�O�R9�c����	������_�>�����!|��^c�\�����50�SeV�������l�_>��\~�N����X�����v95�H&+���)��W�����5�rU
~����5��fa
�&Cj��K�=��i~D����kN~=0��}T�g!/0=�W���I`K?R�H�	O�k}8�~��Si	|���[����7�Bd����7��e�w��t��!\t�����/�����_��ME�X��%�U!�R��'��$�`���'����a#�x��j[��z�
%\�Q�R�H���wOp����:�U��RMN����h�KF>X�����)M��O�K��Ge<�*d�T�7]�D�4�t��}C��@����=1�|�L���'�������=�)9�!7��Z'X���SNs�!����M�F;$�G�Hl�Ep���Ar���Z�e�|N�	��O/7�2�|PB�zi�'w��s�tP�ndMr��5wt�8���x�\���+6�2�M��j�PN�f`�����?�X3jz
!�r����$p�?~B���;��,wf&���mZ�P���!����guD�����js�� l<����l�XX���J��L[��c:���vO�/����e���$�vS[�9
?�yv-n\W�mX��Z79;I��������%	��������g\��u���/���[uS9�o&z������7*G$�-���[7��m~t�����S��M��4�xF�����������D[�9�Z�������_2��!
��9us.�����R�����w����������I����}T/a2�@�?G,�(�K�2��;syv�le��d�� sh�����`�����t&���"F�m�8�"�q�@����4Ga�<u���"E`N����������a�VL45�>0zGIr<��X����������*�I���Ytp����{�A�xG�>�+�����������e�:e���r��0p�!`*-vW�T�aIf���������`�B��>�kb3�������w��
���*��'����,_��}����]����;�Un?��Ge���Nl.^������$(�������R�5����P=G�����n'
[��@`�4�i@�.s���=%���49,�P��g���"����e����)��\3UPiA�"*��U>���K�����8�'��}s�}���R�Y-:O�d�r�f�[��:����������r�<��Q-:s�I.S^����Gg�!���RlF�!)�b�B��y�p����)�R��\��y��X����P�����zu\�j����nr>��]�#������`�X�q�{����Q),��kW���P����H��aml\y@7)\���_gW����.y-�
���,R�j�����:A�������@�5+eP6�nI�"�d��lf	O3�lo���kb�I���k��#�)$�tXm�����f�g��U1�*l�����������T�F�����[����j!O��g8k:B��a���m�&,\d7�0~Bj���CP/���Gw ��er����z^ ��l���	��nO��~��y�����N�i��M����	��]Q�Oc�Bq'��t'D�fh��;��N3,�+H��S����i��J ���n9��`Fm�r ��Xy�A	�\ �a+��IyI6)�d2�0��]v ����/�0ZU?�b.�A����S����4/�UF�+�_�D���Z��u�.	�rg��������<���l��U��O�Wd?-�a�0�\��K�.`'��ophk����C�� ������V�V�}:����WC-������*�[������|D";6���!��{�n���|�OF��6#�0�!�>��~�JR��IUS������__��)�e&��a��z1v0CV;�:;v,�����>�������ST%	���o���|��)�����z�X><������	�)��"�k1��+�p�
����j�x������`����<[���d}{Z
�"��+H��$P��������jG���B��@���h(y�������g�)Su����n�M��~d�Q�2���*��'4�������3#�0������)	3�\��	z���V��/��0��\<'�?B���SG��c� ����s�y�'~��is���������N��`Bs�AG-qzD��KiLzS��a=�����(��M�Y1\���Of������09u��)��Mp�/,R1&6O!��2�//X���Nx����'!z+d�|�s..���Y
Na��
[�W�*�)���x�dc��+x�4~sU�.��u�_��1�B�� $���j(p����u�
����9��jr�og��m*
����D��;����+t�&Q[������c2����l���������Qg��An���7O�m�Pb�G2��\��r����=���q�Yt������ Yb����<j����"?��J�?}��n�a}<!BX��������|��V����c!#`�����E��p0M��X��*V�M�����V�����>����J=���@��Y��+��Y��9��k�F��{�F��{�����rj��x,]~MBTs�d�&��������n�3�y���:R7�����YX�ia��e��<�a�K�����qq����>X������������J�0�%^��U�I��j��eW�y1�Jw@~'oFQZ9E�w��u�������5[
c;�^�������`%�k���
k��DS�����w��6g�#u��d3�5���;_{h���v*��B{��LJ�kq`9�RC����{�8}���
n��|��
��7�|����YB%�7e$}�1�3��*X,_�x�gwU�:|I@C���V�)���jV���y�����,�}d��V&��,]�W\t������st��Q���	 o
F�5�N�����Y=�C�>a���m�fy
��e�:rZ�Ff�Y�-@y��]���q��L��on�j��w%
}F]c2`��w���~�t%K�I���sJ��*�{��bax-�xx]����5���
^f^%t��.��Ig;��ZE*�F7�
VI��k������=�V0��a��uPkI\(�~���f�b')p#t��91�o�~�?��"��,:���W�o/F>&�+d�%G��Q�dEv�C[C��s����q�/��e�r������b6�%@�m��r#��(�_�"���*���#���Kg!�S�<�k1^/�N��pd�+�&b�`�0\�IG=���W��v�F��i����\��`�����@���|[���#G����Ee7�B������=S�������Na�o�g�ZWD�������gm��������w����� ��]!!i�
i|���X�b"+0%�K���b��m��F/BMe#�G!�6DV����#
��v��b�R����r�9�8���U�r��U���j�X�����+��M��S����&L��Q����&x8@Z���$]��r�`�$z�J�j�,�[����[�����0�& v��r���m��C�-��0���mD]���3��f�&��gz�j�����{m,H1�>%t�<�M��)Q?���`y���or4�����1>&����3\r�51���"t��������\ds��a����/����%sA!�b��o��>���p�T+�y]HQ�6@�X��Z/D$��F��;i*���~��.�H��Y�aq+�������t?����j(�����\^�~K�f��OcT��h}���C~�yrIl�S�J%bG<K�s������V�S�v�A�2)�	�i���[{r�����a�c���)G\sG�B�G���XD�
#&����,~<������=�#���j��\S�-�X�������b�=o��5kp������
�AU������E�eA��e���G����A@��h����I���F�6�,6n~�>�r����~���, MQ3��G���`���M��#����l
A����S`�'8.9�~ml��>�{ss����)U�D��S��p�XX*:��*��'���-!�xRpQW�\*F�n2S���$rz�])�)��h�vW\Hv�B��O	��qR�}EM�����o��������fK��)hJQ�'�U�DM��N�ag��B����*�Y�x������rL�����P��(b��0V��������9�[�W|����6��Wf�us�%��nW�cp����J�������u���bX�����F���EP���MmNs;w�fMt+G
��]:�Y�B�<�fw�h�@N�)�&��[h��s�2��p�������@���q	o��"	k�)�����4Z�Fi�:>b	$���H
�P�����P����_:
�&EU(-���N�	�>��)
N������O������?�*1��z����I�����dy4�7U�+�E{�/���c��_�f�Q�t���F}2�y�
�~K��E�*�U�
�C
0�#��������Z��P��F�;�k�&�]��cy�(2�EG��U���q$D�4�i8z����QdTkCJ��V�n��TV	�����tx��b�y����"D������
:|I��XK�������,��1n4����8��s�`#�d��?��=l�X�@"�v?lO�/4c�	�����x�TM��3�@���[��3A6Q;K_�����iC����l�-�	B;t�r{�����JL���<����yfL��7A������G�(y����4p+M�D(w�������+�\ti\�q��P���?����
Xh���r�������9ag{:��;���[$��M�N����B��0��o;[���	zAS*Fal�`�H�����`T�+*�7�N6��);��q����i����HE6�6U�vec'����c���n���Om<����bG5�
f"���������=z�'�S������CpM�h����O%>�)�;dM���/Z0s_3���9�D����Yz^g?���9�����>�	X>���O��U ���%��-�.�g,R;���k�3��zbJ�9�^z��������?�=}Y��]F���{D#��B�+�&�,M�,)<n��y���zT�X�=�����#t�<M�-�
8�Z���lO���k��yu�(�X�����Q��i�t��K�����P�Q�����z-��d-9�_���k^�(���Jy��^u3�0
�����9
��S�?���	0H�
L,���6�\��G�P�5��~F�������4>vV�rU	wscaPTrIs���� �_J�&�$Q@by����1v���Z���cVI=.������t�{�m�'��u}RG��/�Tj��:B��Z���W����~HY~1�/E.5�:��[���s�J�C�Z���L�qW�q����p.Pi�����J����wl�}Tv�^�����@M����U5%zE�f�yU���T�(z������
T��,��� +�n�	�gs��23ad���K�x�����Ymd���N������.��32�o	���B#��.W���`9�a/�#���=�m���!��+S&V������u��jK|{�F�S�U5���"+M���������P37���bF$Gqt���5��f�B��F�&aEi��}��>��b�8iic�G$��Z7&p--��~�c��4��a���fi�s�����0���B���S������@o~|M��I�������/� ��N����������������a�?� cC��95�5��h�g���<�G��}P�������6t9��
I�Y�H�-�"��g/�=�a�r�u
���:h���^��`t�X��_�QQ+�;�JN4��J�����g:��Z����l�,� an(�@+�V�B��X^\�~P��&����}&s�Am'����~�d�G��G[~��E�e_�3o��U�7����3m�`�7�����h���[$�E�x���00��������l?�l�K�Lr����q��hZ���(����������}X��������? ��jm���dM���U��R�F+��fTG�� ���L�j�����TG����_.��/������)��A'��GW�ib�����AM�F��@fE!��qy�{�)���sP@Wl��m�������mm/��C.�TY
�h��Y���N���.����EyhN������aw��by����������?��o*��cx	|���7VV�z6����f{��	C/�S7���POw�����B�P�-2�Vy�>q���Bp(�����~�E�B(a��q��m�"�9��*��be`�Yq��&4W�
���X�	M��:����w�P@�*�<���Id[(�!�����}Vy7sG�����������m��s�����|
z�6���i��G�y=��Y����G�h���1�k2���)7�~{�}t�}����m����gN��Z"Y.���:���b�k9�X�|�[������nK/F�K@m��}a�I1���I��`������W-O��m	=�n��k1v�r��o����V����v�E���t��!P3)�����'�r]�D8?Wa����oO�X~��f�_7ygN7��_�$����B<%�Ba����c��PCbRt����^��s���Y����D���aL)���(&u���y�����<a�������$��u-�
p�:A�U(3#���tc�Q��}��E�g�zU�����z�;�j#�����o�k�w�w0�^����TW��l�'~��5�b��z�_nob���\�h� ����k�����#|����a�*!�V�z;t�5���DM��Msj�Xu�w~�[�z�����c��<N,��O�����#v��]�piZw2
���z�1��H��Lq"a3o���?�3��`�b�G�c��Ad��_m������'$L��`�?�33t��-"��x��"�����2g�!��6$j;,�+�t�uFH����������k:�@�x�yK7���������n��=k�1����aF�����a�xn4H%���������[�|;�
�Ju���R��h��=��#)�\��a���0�M���a_�7��t�ix�����jv#]���u�M��u%9���}A������&�<>�y_��4���6��
_)��?��T����f��Q��Z6&1-����'����{����,>���hy��e�my��7���Wj	w\�b������RI6|U����ewpH�?�l�^I�y~�l���Q����Y��h*�S�+�G�qb#����o/����S���������n��^*�`L���3FL��2�:�3u�u�d��),W��6>��07�{��W3���6� ���o�.�>��vtT�����'^�V��S�Y�C���}�����W��:�����<U3lFW���t�a���h�u��-^l���VT�>2��rd/n�Y
��M��f�$�TYH�e�u�����r����4f�yzI�@mu�=��b��y5��B��q�������r�1>D�9>�FW���S��?X]
�}�|_�E�.aLL���"��1�E�`���Xj�������������@w��N�<U���A2`X�l��R��j�<������3y��B8'���i��C*e}.�n���������M�sX���9��zC���C&��X�VJ�"���'�K)�6��&��N-h��%��Q��/0i�n�"Y�u�����s�tN�F��y;�Vk�&��|\W��<���<�,��uc�fS�*��T0�pws]]'�
`Z��v�N����j�S�
���_���a6����7-G�C�gq���sB<��������0
���$���v�AW��fS���bEg���hO����J��L�*/�Y!d�`�ub#��b?�������Yx� VS�Y�}3�T:������]�����TQ)����m;o�����l�9�����o���m�r���'��;�m�����mk�R,�ol*�`y-��y�J�i�q���]��tEB*6�r�a��	�/����������d?�jJ �W�������k�bP�`:�v��"�q���<�]�t~�C
��d�������2��r�Z�M�W_r3��)4�������d�!����X�����dt�af
iHT(�����d>*��
��H�G���QA��L�n|��D�!����W����`�j,Jr�4���I%����c���Pg�����\l10���S;%��/�%�vS��F�LBN�@�*(�n
�&<���be����������?�N�78�E7]�I�v��vl�����Q��V��k�}�9�����T"����Lu���;d�NHGUy���Y>�>�(��0���}L�G�{����Xl��k����5�CP���H����]�aop��y�zp���;n���[=.W|�;����I����+�c�&��� �@�l����K]�>�QW�x2��,���E��|�!�Ug	�d@�t|R�3�j��D�D	�J�U2v��H��+]�U��z�����LZ8�NF��Nn����B�1$����4T6:�������nZo��F7���U�sT��:����/%�ld��)aJ��2����!M��7�^�?-��|�Dm�P4X�����"Z����/^�h��<M�[ ��l���)=�n'��x���D��us������t��8�l��^�O�]G\���KAi-���P��k/�o5(x����J������3T������G��"UTJ�2�%$[_[��/�mqI��;+|%�rM�����{��$�������	�&�EGc�>�l�l.[|��9�#p3���N%Y?x��|�K-�/����|�!�q���%ss��������<�\O��]����$��QcK
.B��s`>J<�Kc>�d[�hQ
@v�Xg�~��	��cV���W�Xi��4��E��ll]E��������?-Ug�8h�~�b~��f�! 1+,'��l�+�L���]f[����N��������Xmp�v��
]�����;_%���"�'��[�s>��w�����������j|gs����B	W���m
�h��Uxv�~u����`��	O���e��2#��8�������'�
�3�1�i����R��l`��t-k��e���t����1��+���~�T5|�������m��e�e����]�s�Qo��������5�D�������o�5�_I����m
�P�?���+����Q��p��S�]�O������Z��`�[A%�n�����E��:>���[� ���K��M�BU���-���s�/V���/;��9h����rG}Uq-�����������-�{{���kl*n�s��fp��o
<���]�r�t�{i���<���3����C*�U/���a���	��b~y�pq���S�X��i0��l��A8�&��b���a����<[�L��9,a�F1!�1����ju�W
6��n�.!�j�c[�l[4/�����+(�$!�%�K�VM�uL=�nH���Ko�v�FC�F����G�*8H����������y�~M�T"���[�\C�g�x����J��*���,������UT��QUO������q��43g\��;#"��kx��e����]������M���?h6 _�U�Q���3��=��0p��C&c����I-b��t���Z9��*7�������U��1���nN��	���!?k�l���v�|JM~���H�S����aj��=}\>�q&4��a����<�� 6�DrK���)����1���)�����,��/41vZv�Np:��x�G��2._�G�������O]l���X���]n�:�x�����{.��Ap3�Vi�8��bn���J�D���2�m��%�E���!��"oLaj�7W O�
�q�S�/�	��w��P^����-�$�����e+�6?*P4�
�������D�Qy7��p`��	W}��-]��m^[�
�Y'�{����=tt2f�BWM����d���4d����(/������	
h��O���������/Y���Z�g�7<8f���5H<DS�-o=�����Q���@g~Z}(���6��^&$XJ \p7G�E�D��
�}&�`��x`�;�fo���������8���K��'l�����`X3!�\!�z(;��;s��G��������?�0y�Q�Y����<����1���?�>�j�q����H��fOS��P��Q�K;��J`��Fi�� ��t�j� �f���'`��������q���-��G�"Ow!�{U��B�X�<�n�f�X���9�����TT��%����5n��k�fp��TG]B����������_�����y�m��F76�J��^E����Z�#)pH<-������&��$�l_�4sS�ay��DC������]+7t�x^�����x���|~�m��y\3�zy����;�!��%�]��*�9�Tb��*m,�����a����������
�G�����H���Y�n��X�Dv����������z���g�������v�����<�,���!��\0�i��6h��ok/��XMU��|��U.�:I����N��anQ[%��*m��$'���>���B�k\fC% �����
��%����^8' �b����0���Y���(P�,�n��7�p�B?���5�k(�����������Qjm1�pS�Byl��t�@�������S�0���D�AD��S�!�{�'���DMU�|f%�T-����P�]�,`���{Xms0\��]��������5���[�Q�3�
�>�� ���t7o�u�u�	�����70�YCt��Q�t���9�m�K�|�}e�(����|o+�c��3Nv��G��l�e����M��F��hGW�*E�����e_��WI&��y��>�����-3�b����}�o����~
��FV>e��B��_K�j;� �n>�n����@��k,;�=`{���8���N�u���,*����� oN��4V�6G��a��� �v|��M��X~�������5B�,�������#���n��^BU����:���1�AW�wl��h�ax���i����Qo����*Q��bE���	#�(���u��}�n�_�O3<f#��o`U!B��(��{m�*�;�@��tQS��{���+�6���7;���%"����	��6���n��n����|�{l�N�?h��@h��
D�������#�&�}������n+�� ����a�D��bV;�<5/���`b2�PY/��-��;����2���[�x��!\C:z5A�3}Y���E����U���{j~3�v?�S��_{��&u���&�/k�i 0-$1���<n��<�W�>mv���:����ym���
	t2y�����w�/�����cM���-��m�~����T����M�}^Su�\4x�y��d��-�d�0�6�}vt� V�o���MNSa��e�������y�d1��l�n>��)�'
�w}q31���:��7�M�����
������0��(�?���F_�P��'�����G�V���"<f}[-m������c��,�S�7��,�1����7Po����l�x�i�'��} 
g���L������*�Im���������~��:��-���2�����|�4Z��������kkS�0w�<;���Jf�Ao#	}.�\4J������KZb]�Z��v�-tQ�r��~p�I��6>.��-N��]W7�^C�J(`�����`������n���N����p���L���H���
k9)��C�;��a������MN���/���T_����3=��|XQ�a��SNv��R�����F��8���2G�g�U�K�4�\6���&���}�0�e�U2�L�j��lV@�������`E�z�
������4���kf��?�G/�)k��>���W������~�=������t{��m���������t,��2!zr�aWT����Z*�*�����m��S�A�`���!�a�}�]�[+6s}|�	M��Hz�{f��9$��@i4�>-�	L��1iM��*D��g�n�8V�E��z+.��7SU�8���9Z�f���������>���<��E,G����Jd���������v���+��@�R%>�
o��F����Xv���~2:�<8jjBX���(����k�}(2AvSz��!�'Q�:��z��T���Aq�f�Rx��&��a���X}�� �)��,O|�Yw�����
��<��.��S��\/teS�8�x���������_~Yz�\�?!������y�����o;c�aA�I��v�e9"�V��Fff[M���JV��:e��2h���BOadiW}KUY��I&����u�%��@����X��mT���.W��H��,������]�p�]C�rZ����l��y
�� n"q>)��X!�c�,G}vz
H�j���2Y���!�K	�u�4�l+������[����U�k�2i�
�]_m,jR�3'�kd�x��S��@5y��{����vd�5S[��y�N���j��4
vlY�����h����^>n�o���Onr��'*H�[tM���p�W
��by��f��h��������!e���
�?g�<TV1p+����JC�4"�n���%u?H#L�@��m,�8dJ��\���H��T��5����W,}q��t!���
U���;`:������7O_���n�>�6+�����mEm��U���*����4~sr�m'!�����S��5��6��75�n���Ru��Yh���S^����!J�������C/+�0��O��R8�a�p�94�P�6ywSG��>7!I5l
	��R��x����vAB��r��$����J�[q��d�\�P��Y������������k�1����eo3�oY�����&�
��:tc��m�}���F�����_�Z��!�sM�w)HI����_\tjv(B���F�s��5���wI�^E���_=���lx�<�O�Z����q���~tW�%�}3@b_�UX8�*H�����j�JlDo�/���@G�@s��x>C��5���$�mUg����jU���{'kErS�����i��U�X���.��x��U�Om�3��$>=c�[����}�kM0B�lUG�L���?:[/����g�������������n��b����T�a�N���V������Ozu��/+	�1��K�����~��8!5>2����u� b������c��|!��9��hZK =&��g-��N�4����g�%���2���"_�+=����0Qd�+=���6>��
�@��.�k�3\t,K����YF�"v�xX	�s��_��(v�9
��j����q��t����_���B�}}|�nv&�aQ����*��<�"6�q�d����:~�]����v�h�x��!�	�`�i|,����s�����s8�dU?K�XB�n(��^,�;�	[x�ys������^��DP���R��?���~&MIV;v���4"U�m��V��1Q��?qb�F��������?(�W�C�Vt���[�!���i�s�/��$�qH,�[�%�� ���w��^
�dJ�o���i@YZ��_.��z��f���s	R
H�IU�6U��D[�"�,=���.�1&�x�
��ZF���� *%����T
>�����UW��DK������c0,���p��3���������KT��>S_�U�������@+4�����> A��=�t2����/g[k������ru�4�������}�o���UI�;B\Y`}E`��h9>B)�{�#Ou���uMU���al�;�n��v��']R���)d~Xo���~����y�f��#�s.2�J<��A��^�Y��Wf��{��jf��/���o]����;�!b�.Ku$MGu���iT�����l��O�����Vif7�<- ������N�6�;5	}1p��e����a���3V�s��p���*r;����Z���`b8��lW����|<Nv������|T W&vqx&e�_Wso&K��X�
�9������n�TT"��Z$�C������U���I}�������5�!� �3P'��G��y�PU)�o�����F�%4*L�G�	���
��V�"�KL��
'�
��C:�<S����
�l����
r�p�8dM�kc����0/O�:��@�*���w%;�	�z'c��|���f��N]*��|:��zv���}�[�W����'���*�U�[^NM�M��L������o���&�y���hz��-��}�rz��T�����]p�]������~K����
]��E]-���:�n?��T
e@���k�OF���~0�`o�J"b��x��n*�E��-^�H��.���Pc�����������	4�m� ;�+��Huu��S_��
�����%�����d�������T���C�PH��~�����K3b��������x�|�"����6���b��l�2W~r���Xu
Q��L��������a
��.��������z���>�;�'dw�z`2�8[���a\hLfAO�]��5�cC$,����0
��:��qx�}�������j�1��S���M��{����W�w��W��w��5��FgG��b�I�t�����9]�<=�@�a����"�S�!*��K� {��%�,r���q�����e�jf_G�g��g�P]�7Y2Y���9`�����*d]+{#pw���@����z���]���?�fY�m~w��v�d;�@�7��V��
��PYbc�S j�x����	r��������A
TI�A=�{h�~���K�Xz!���R��U��((�}�>^�mx��(4�w
�0#�s�*��y��lQt�b'�Z����H��v��a��*!������n|���ijH�����2���X�=��@�V��������p*BS5�F[0bf}���&<e9h�M56j���0xP	K����1���TB(��i
�T�T�fC������X����tD\�Zve]��t���"x���)��"�KiJZ���n�g��0�#�st_�Q�d�)�=�
q��0�cw�%�r������OWi��)���Z��*lY8����k����s����_-����JU$S�� 3H�d��[cP
y�H����>������o����H�o�L�M0�.0�=�f<�B���o������Zu�����mF���
vm�������� ��b�"!J��P����]��K��	i��� ���.2�=�O�
ze��$���s���nW��I�)��T��5�=}V�&0��[\��I��~��:���;X������c5��
�g�a~R3:C?t|d���w	�
���]k��W��<K�����������QIJ���k^��l�,�X�dj��x?��Y�khg���>�6/%o8�Y�N�>�_0z��g������\Ic��<���M� SX_�`���"&A"s9b����c�&w>����P��@:N�
�����;����0��Z����.�:{[������QIB��o���k��VO}d��7]����sd�� M�	��~X�oYbe��
}���������y��N���!���s�������.��U�/tfH/��Ku�x�QNn\�dVCRi�x���"sa�M4{O�*[7����h}]h3)��n�s�b��6�g�F��>������:�{��v���c_
|]"�X�%
L������]&�.��Y��8����������}�J�&L��E�x�4�<F�K�	�
nO����Z�������TB�C�-'��^��������B4�(f0z�_R��U����33�o����2����H������8�W�V��ja�x��#We�y]����������{W��X����)35���_oS��������n4od��Ryx��rw�������T+��`����
Z�t��c�����
�
6��|��\�~���>\n�7WZ-7�%S_:�ILD�'�D�������������j��/���S3����D_����j~1��D��c#��jT^�M�dmP��Kv)P�5�~��i��,��j�<k�h�����-F�;�?�-P�-~?����f��VX)kv���W/����?-�7�c0F�c��{��8&5��5A�<`�h}��k�n��!
O+���|*�5���n���H�($.,�Bn�n����������Y����S�}�0���nn����t��@�T�qx��P���9���%@�\��9����W��������y|��VS�d�ov�����Ssz���������l�dFO����>f����0_;�G����n@�����K�/�n���Dd�����y��I���J��C�IwB�nj��;``��UrX-��w����z�� �$Jv���B��37�P�O���^u����B3	Y"7�l�a���vT��B����HV��:��HZ����t������=BP�����K����U����r�R���-`m�y~�eC3|@�otNp���9L�7�����X��u2����M���� �vJ3.nc!
�<�'s��kb���������[_�c�%Be�+��>�����z�����%�t��������f%_mj$�<)���g�:����[��������=N� �Y�ts�������:!�y���a����6�p
���i�k�_�%@�6S�?�f��>�� �{(�`{l���������v�lhQ2Oo�4cG�s��@	��4�����RIi�����8v��b��������n��6��```[&��B�
�A[������WV�\�&:(�"�-��n����j�@|dq�\�6�����C�������������0e+4xi�@�a��yP��h� A��(
S����A�� ���������v@��7�1 ��W`b���� v�����<����K�0������A#����Wi,�l��N���>�0dzy��~EQ
=���<����g
�`~#5	 29�7q��}
�86��<��V���������s��f�+��_B�H-��Ir�ziy���^��c��'�0�3=�H����2�o
 ��w3#��^������P��M�^��<53pk����J��|�y��������M\����UHV���Pli���j�U�����u����dNDoH���2������e�W�I��a3���1WU�	s�bX-�@�$������7��4��&>1�2����s.��0]���ojs���� �6����X��4�	<]��g<N��3��y�
���:���c�S�_Anz�������;�d�;�N����������g�;�g��9/����P������N�2XO����d-����O�?~����/�����NI���H}.��f@9�ez�����z!+��t:�r��,^B����Vq8+l����-L�z����?>���1���k����T���rU>������������g3j��>�6��8�
�?��_���u u$�<��QuC����7��`�k�{���������T5����m$r�v���<�������FXz
� ��x��.J��K������'��W�=7��?O�������o~���)���\u��S�{U,1E��RQ��#Y������x�pt����a���U7���C�������""�����~����5����a�=������Mg��*����Df�"
������5��b��������J��-�1!,��������uo����g�Mn�m�3�B�q��;N[E�4�z�A���-8*{=k?�=���������L"!�L��	���ls�"���Q��Z� S�e���o�������f���-5�!QSb��O���Z�u L���L�i�)��}��F��O����`KC��$�It�x�Md�;>I��?}�IFxZ�}�X��u��k�����Y�����.���^�B�m��D�r�{4V.��M�U�y���%[�����ooRe[����
���y����y��w����v�zZ�?�?���P�v�����`�10��C�5�a����3��YgX�����w��WY-���K����l26N��)�	�w���g�?.�������(ss5F��Qi��"V�t�(A���/T�VW��b��M�]qa�Lf������h���*�'�P(��M_��d���Z�$�B��M"�.���QDi��{jN"2�<�<��t5aC��|��}���T�e���T�lG��~�]#�/�����������#:�;��|;z�FV�>�0u��X��R[�M|�}�j���=)�~uz��q�1�9���;��T�&m<������OQsX�BM�.���������&��K�A�2�e�U(}�)��!���'r���3`�Y��{\?�P-����a�-��R=9������6&v?>UO:�
������T�e�u���|v���0��P��m�*���!�l����hz"v�e�%����}K���wHY��=�s������+}	"��u�l��zc���F+����Yl� �[�B����S�����*����QMG�?��x��5d�����������l��P[�Lu�8,����wM#[$�&�V\--{+���r����g���1*O��ug0b��FpL����I#�]�����S�������)�c��,�A���8
f����$��=~9>���h��3~�2�9+�[<�E�V�����.�
S�����(�UGa��.�t�����:'7?,�y5��e��D�����g���U!����3��Q�&mS`2s���U=
����C�9���[��Un��&i�+"gJA���+6,�2�@&�n�F�4�O��N8��
�#}���4��7�w����o�/��9hC���~���Hps���c=��8�N���Y%�0�f�P:���[,g����U���ND�Gm��\`��������e���#�P ��T�j�&�G��T����
+L!X�vg,�k�w���i�$>B��31lF;KU����<���� "x~�����8��=��cr��z�������R�It��Wp��M��p��T�������������OX���v.C�}��V�l�����9ON���z1N�m��$��#�:�'4�j���2���]+���6[���V6��<����!��B3G�����M=hX��vn���j�?�����]	��Zu��.��w��8Yf����i��=�^[�E��S�a�7)���VYW������[�j{w���,,see��>����/����z��@��f�t3Ra��g��yU�����R�����>�eJ���*�bZ==��|���0�ogg<��-�5����3X�f�u�
n���Y���V�1
^e������R)��
����[�O����%�����������~��9�~\���h�=��<��zC��9n
�i6O
�$�4���`e�[�N��������y����G�}�`�������[�x.X?��pg����z����G���?o�u�<�c}<����R���&�y��d4��J��E�zx�0��C@��Un�`�?��s\�������k�~fM|d��v����V��"�j��] t�7\w�]^�V�����E�g���B���+�y8�0Kw������l���9}�M�<W�	��&�u�S�T@�\<���KN<e�7s`@��z`��W�W|��n�h;�D� R�i�k�.]Ux��C��X�s���n�����w�� �
���������W��ecW1�|NO��t����(�L�Dsg4=�A����\e`W�	L��t�{E9���Q}���d�*_m����>������_�,r����W7���|q��^���J���^�W�!-�^������W7B����p�������E��<R�x�x�9Vw=L�*HoF@
�I��r�6'��3'�
={�c�p���H�1��K�m����`�9���]5�T�^��������*����
����F���]���{~'�
<]����|������>U����^tl_rY�������>@Xb�����BBsqxm��W��� �G��_�f]3��v�+��y���jb��M^@�]�&�O����[2^�}����}B �`�.���o�H("�"�"�:=v�,?���\w
�.V2�8���w9YF�a���L}����������Q����w�q���0���[g��X��'��d&2�>V;{�<=�z�!]������wyB�9H�zN������*z���A�fX�
w�}c�H�$H����x�����R�B�=Ef���t��C���X�j����h���G\�������)Cwn�^�:��B/qm	�=]��r�%'��sJ4�1�R�n�%Q9�([����x��I7���	�76��K�Fr�d1���b��a�g^�,#5���z����?P�]�9��O,��sl��cr���-V+.���&���P�Q��(�7���i�����R�e�9������z�;/���[���RdV!R

�`~�>T�������1'��>h�N�S���xh�Pt*�mj��^kfJ�a��\��Y+�!��is�0�5�Z��� ��'_�=�S_��C��Z���J oY������-�I}�J�Br��|��
��m���G�f��yX��XawV��������3��}����c����"�Y�%�g�[/�������j��&^�-��dG��a-�Ya�R�|�������z(��e`��-�H�����6[����o��9�=G�D�Y:��F��M1@"h6o
�_V(����X����_i���N"��7��V1Rj�D��\�"U�hJ�-"	#u�j�'�:�&�W��,_��l[
=>^'H@�TU�����l�A��f6��t��Y���A�4���&D.]gj�&��s��W�~�����U��2f��i�c1�R����"���4��H]g��M�q���X��s}�w��(
����\����f�lR��^}��"��k��-"l����+���Z���fWj�y^��Usr�oIs�:��xY?u����V
pE�3E���&V����"gVg��j�.�YC��F�LWZ�-��|J�
���$��H�b�w��f���U��|�����L'(sv�!�^v��v6.�A,��N�TE:�6v��a��������G�|SV�5�1�A����������N��8����������i�nj0iLE�
���&js����������_=�^!m�^�w�Uk�v������a�yM��3�]����� c���=b��G/T��(��9}��f��X� {�4����!�
�'K������k�M.s�����.W~�F[XS�jDPY�K%=F��Y<B�/�F�c�������
�T�~�32R���
b-E�y�����/��/�8��A
g�/o!�s2��5�Iu��1��QB�K��P8.��0c��X�+W�;m���!��B�g�ay���q���	�3��U�����35��&W�
U9���!=��J-L(��z���\Mb�`zvbE���Yb��i�}\��N�i`�0t�E����Uw�������pU��S��^&�Av��p/05�*�/�����]�6��J������A1yyB�����[CP���y�����UN��e�ey���90��[����' :��c8�rd���	XK�4�Nv���*���P��k/��vd��~<�s����_��>e�
���%�tx�U2OhC��� �0�/�%u�G�F��m��sQ����v}.Z�7!VUw�o�����i)����8����Bf���$Wd���5�x�-9g�7o�b��Y��<���������V��j��C�Uv�WqiJ��+������w��B�-���t�2�
�A�9Q��vF��<���m�����r���-�����1��W��h�6�Z��"����|H�K&,�;p��-,�����8m`q��uY�����Tq4RX$1;�!w��bnp�t����0{����@�l�e��k$���nqs#�1�@���3��fRhl13�y
��tu8j�t��\���%�wJi��6Sy�nF
����tg���l�p|{R�Jf0����?L�`�Ao?d�$v�aG��l���
i���sB��y��{��;��$�W�C0������I�t�\�`k6�)�V�7�a����F�#5����1��U��4b����-�+��J��$h����I!�����tK���M)����(+l��9bK��������/
t�z��� �I�Bf�PI�"�
���[3�nS����8?��%�D�[���[����M�a�J"0{�����.l@"�
�lI����F��K���ki��1����k;U�D�-/��e<��(F�U�b�1r�*��� �s;v���(n�F���q�����.H�W�q.�����n��E�0Y�}-~���������i�2�r>}w6�+
H>qj���&ly�0@r�S[�v��6������d�;��1��yvA�I�n����HI�S�����R��A�}��	�
�lev�/s���796HO��R�Wm�7����������b�I��+}N�v������y�S�����F$B�w����M�[��.��
�q��j=���m�p�{�[���Z�)b���[�VG���mr�X���������?�?V����K�Q�v�$����x �a���� "TP�+-��TZt�/<u=�r�6K����.A	������~�il�+nZ$:��*�zHB�i�,g,2+ �n4�dI`yZ����3����\.�n~\��o��d��=��a�n}����?�q@@���<�S-qs��*"F��>���`�E��G,�y�>u.n�\�D�o'X���T�
6A����'�����Y����4}j3�P�j�
��?fj�,�1��yl�{5C�,|���y��`�I76ZBU�I�:���)��>,��M��^��H�y+�%����E.���h����:�*�%@
��U�a��PB�_��"v�f���&M]��OO�!�����f�
O}`�L����?�S��b:�*�Esv��k�h`�xG��lu�m6"�5_�Q�X���A����/�]���I`f���Z������[u�k�+������v��q�i�%{�j<��v��U���*|��G�D�P7
,Y>�	D��C�����~L���[�������}���\}�pX�s~���``���Vu]	�����FE?�Z#;�u����K|�+�Y�2e�t#�����@����qpb��whT���2*����e������S+���	�z�d�p������I�&`#&6����
r4 g�w/�e�-5gh7O������Im{_��	���Z8�;<I���Z9�)��;*�v����'y�B����9+�aI3���b�1&��-�H��,�!\}��E/���$��P��"z���]����nZ-�C(�T��"��GL*1G��a�E�Z�~�kYU)x[u�fX����P
�����
/���<5ME[�gzV�$,����\~)I�o/����B�2EiU�z:k]�4�B0�x��BbU'��!r�i*%�'��������l�x��l��*���b�`�����n���@��?����h`p�`dw���q�)v!L?;������8��?�{o���Y>��+�p�'�(�A�����q�'�)[��/O�:��s�����G3�8?g��(H���;?�{��:�,R�zsR�1Ss��u��(�;�����/�X=^��d

�q�V������i�"���V����I���Y0vb����`1��d*��NeiWY�a:�Nb�S����-�(pp-`;��N�_�P>>�r��9�,���I
����;�l�l��r�]
���|Bh�a��kj�4���qb�JW���Sh�k<>������4w��������=m�%�xe'����j����}m�B�lwA��~[������R�0	��W������6������;;K�����m�=�G�t�e���{������|Y!��^����|����W�-�&Pz�b4����i�j |�?W���]Y�513,P#�@��d0�e1��T;���#�9[Drya��}]qU�,���?�tJ/_�������q��rt�=���,	����i4�
����s��J�zi�����^������~�#�E8-=���H����O��1sf��!w����J"��kb�KXj3� U,�Z��(B��k^}�^���*�a}�.���ZN�"�����9p�
w���V'��_�i3	f��+U��Z�70�������w���o��X�6�I�J�w	�Q���z��-:���5;��Pu:l�{/�@���P���E�,G�X���2n����~}�����>�)�E����
��!4�4��VQ�	�R�b��h�4#w�a@���4O�j���C�d�������=���z�{����t��S��NzWCyh���}������@��
���r��������'�����R
��'�Z_���'�0�em,��m�>bM���<$�Z�C����7;~�FW�;���o�����������������m��?�1��^A��cF���l:����`�s<�X�S���W�������l����s�T�����l'��)R���"KmxBB![�"f����Ep�
5(�8'�Q���K�o������|����Hg����4�I+���!	�l�o^�a�Aw������5��{=�]e��j����y�&��-\���7����5�W�G.������a�������M�N���HL������mtT��<�J��z������������+�����8��0d>|_����
���.����+
�Ic�7K�}�����������;T���(l��c������K+>
����o^�u�����#���������RK����,�b�Lp��YpC�W�"���x�pX���3F��ig��q54I�j�*\��������;Qsna������a~��<��<w��N���^�=��er�a7�N�u���6�O�S|�]��cS��0���X?��E���|\�V��~�9eSD�`c�+Y�*_e�6�M������%D�9�����������f��f�$�r�����i�C��B��"��xu�
�o��
�d���}3�c:\:��`�l(��$�p��Q�u���;�����e�|�[A���8�B�6	\d�c�/���?�-��`�?�(g�uR�/��!����F/,����d�����TS��������3m���.�������h���5��-�������_~^=}�iO���X�V)H)V9���������3B���]U���(!&����:6�F���EM�C���3O��.�� �f&��'���460��:Wd�L����=DsS���M�>��7
=�@v{�|�'A�oT�m�W��3*a�(���i��!����Fz'`&���9#�4;��"��d����W"{;� �<s"�J�
���
��$�����RV���M�n��jj=oC6I�Q�I<wY����*�\q*��y����z��hL�M���:������S������f��lc>���
g1�f���p���4J�"��h4/�O#f�������������������+�$�J���<�$*%k�K������e�0��(���!�j��5d���h2�N.��z�Xo���v���4�G���fr������W��\T�0���M�h��)F��'E�1����Q�������
�������j�(���b9O4c���bv/�vcR�3����� ��@��]	��g�����y�N��}GC���x�m��.P�B���B7"�,c����q�%����U���D����i����^�&7Q0��*&n���`�]06��@��<��i��?�28Pwy�g\��O��tQ�I�P �]�l#~�C
K��]��P�<k�t�bz����$�f�K�i�����o��gS	s���.��]���m���S����g���=8��!�;������<��g�sSK��`n{�:YY�yee�=(������B�H�9]� �E^���k2��C,�:��w��'������~:6�T�0��b���	����Xo0s �(!:�>v	�9
����o��;������~d����Jw�_�T����~�n�	{����
��s5�����3�����d���p�z!R�9��m��9���{�|�yz��I�v�rx|;_8��������K�I���~7�2NCZp�x.5�!+��l�l�lu�;{-���h��
��\����
�]���{�sW9��Zw����)p��7i#������I��}�����e���@'�~1�1|���[OG�������.wm�����������l&����
�$�����P�u�8�as��7�\��9-O����IVP,b����A���|�e]�V}j��J�3������h5}�P��B2�������I��
T�K�����yx9@[���/l�_�6���(�
T@#���I(�$?�l����*lgx�57�_��"G���(c �!��]a�u���
��r�T-������;O6�������	�5�W���GD6���T2��n�e��$�V�jLh��d�$����FnM����Gq�O����}�"`_|n�U�"(5����c��H���k3���t����n���?0r/w��a��Ns������������Z������
��`��=��E'Gf�d`���f���A5�
lsm��
�'����\�n��/,��~��NF��V1�>��s1O��#��^6�w0�:���]�5�!ba�/�������"���<�F��D���|a���5c�zW�z|��t	w�jMB������N��(Z���m�|��_�� �	�e'	2����4��r<����|>j��eW�AZ��5����	;���*a����h��#"�(�$k�I����G�C�-��\����m�"��Z�
yuN��?/W��vy�c�S�=�?����E�-��e�`f�o��Mm�0K��K@�x3H(r1�6[*Q���Z�`���h����d��`"D!\0���������I`4EO��/�
&<����>����������+pb���h���MX����H~�a�\?����BYK�� Jl��9e[��^3��uE�������V7����_��b��[O5U	�9�`�����5�C@�p�9Vha_F-N'����V�:)WJ��:U|�����R��i��i)�q�'�F}���]OZ�����fb4��h�M7zT�������w�:�P�CZ�[t���#�p�0-�pfAF��r�eS
Y���d�Y���:�,��Y&h����0��t�
T�������5i\XXT��T]�6��e�W�#y�{#M��!�OYAFh,D���pw4gX'��/�L0x�w����z8�h�2]�@��`��e/�4�I���Q�vD��*a���2d���XJ�p��O;�����z.5�"TQW��U[� �StwF��p��+��|����X�������a������J�J���f
�U�������q ��:9
d��W�������c&UQ�������-��j���T(
��a�MB��e -}�&ew��3�����JDC��'��������7����0s)Q�oX�{��TbK�r����o��V�pF5���~}(S���;�(�����+qh��l>8�j��j�G��RUt���5��1]�
��_���t6�d}x�e�X������L�h �=����S��NY�y�J�	4�8�"�"�!e.? �G�U'�Xn��Z��ExC������<_�R��&~���_d�2����������MtJ�!������4DC_���ue�>toX!�~���F���y��������b//�g������Au���=����K-�����,p��o��7B��wr�]����tW�L��i�\Q�����'Gz	���������_�%
��p�N������f�<�v���(1_��]
tY����m������N��.o�c7�s�����c������% �s����G���E�`���^��q;����P��@p��+���G��Av��T�`�J&|�Thh�X��'Q0����_�����
�%����=3�'F�04�Ix���s��c�8��Y^m�F��C��WQ��zN�m���@Pb!��F��!Ru������U���*P(����i9=<������!,�Z>�2����p�:�\gB���p/pI��#Y�t�2�����:~���X��'B����*Gj��s�<�����4AVY�1F���q���Ka/p$���s���$A��Zv/���VMm���S��hh���9��^�;�2�;7���Q�X�e�/5B��+�z�C}��$����s��G��) wYM]�^��%]9+SF	�#��x����^|!�v���>c�V�BW��i=���)�M��p�6��/���f�o��R���=��}��a=/��c6�U
�7S;�x�F���L�%H���,��P8���A��B�:�9��m�/�����(������C����)��iI�.����������i�2BO�������yE�����4���35��*%1��}z���,���V*!u`9����:+��w�������������������V�R2E#���E�s�\�v��f�R��������w��:fA*����}|�i����K�i�%������������f�9~������"Y�A���w���7b:�{z"J����4���L�.��0�n�j����m?����c����F�~������{^_/��2f�������!GYc#w��V�V>]�3g}LS��d��1�q���r�y�Mp���D��f215�����d��7p��_�?�Y2vv���|����j���C�	� ~����m��L����[x�>('���d�@g$z���8���$_��S~���������$��+�80Z�/�3�3�m��iu�jM�&��$�
*d`u�~��'2#�	�����4�lVb1������C�z4�P2AWNL����n��4B�MD���3������/������`��R�]t��Tj�������t�EBLa�!&f���T|=����%F�$p��������`N5,�
3')�����//���C�G��2]��^�����4]&����y|
����
����*�e��l��t@f�,q����t�!
���H�|��{�>n�z��Q�>��5�I�.VK+��G����Va�P�w��h%-o������n�-]5��+ h��-?�����O����h�@sLfK��&
/}��b�,�����#u*P�w������s
����xU��o0z���-��7A����z&�t,O������#W�K�"^_M�4r2�u�J�m��_��m�S�|���|������?���5�������������g���.��p�t�F>�9��#��
B�=�����A��?c���|*��$���.;������C���f3*��������x�F�C�;���G����/C���Z%?Y�� ��"_����/���q���L�z��7�WON�R���<~M�����a�H>E:�D5Iq��8]eOl_���L�=��SQa@�?m>`kn���k�^�Q���Z��\�5��"y��Q�n�|Z[p9.nF�#��*�����C�����Zj����Q�}����B�H��8t��No�������CZ�J!O�fs�Lt�m�K����Bx��rD�I�>�
��TLYWX�B�#��V�5W>��C����9,�f�4�j&�3*�k*D�i�@}?��i�����)q��Yh���-�K)����+�������5&��[��\��lUzs8&n�|���~�ov�����Qy
t�����h�z3��L���b�-T���`6����Il	K7Ea�La1QzWa��E(��Y�+�4��H�
���L;�;r������j��s?�CQ���Z���<"v���tK6T���u�?������Y��p����
�e�<M��T]
�)�S��?m>���/XN���j��ivr���K��a{%�xC������9�	�0Pvk��5�5�,+�
d[�\�&:1�f�e��(���l�K��wn-�NM]SH��Z��7<+s�PI�/k�Y!97��7���#C���2J�db��Vv`\Y0�#���mn�%;��o���!���6���!$XO�5_f�&"�'��l<��	Y{\��C��d�
��E%�+�H����5���>��y��0X�Y����.R���c����N�c�
A�%L��>l�O��&"�����sC%p_�~�<�	v��QUEF�a�;�&FD�"Z�F�Fv�BD���)K����#�N�;�s\������-��mh�o�s���">2�>���=�����$]��se;�ZVW����J"�&���*�8��~;m���.��x��H�[)�/M�W�H�s�=��6(���
���*����l����md4jf	)ke3�+�L.q����5� Fk��8��'�9������=��}��+�Q�����JJ���R�e�[��:x�j�v������2{�S��(.��������4���JR4Z�E���ZE�m \���������-)^H��,
\
md�y�(3�
�����D�g^�E4CfaBw	��	���%�\�jr].�xB�y�e{�<m����]��L-?Q����K����qp��+'�E�\�zM�bos��3^&�����]��7_
���@��c���
���uW�������s�4J��a�u�8�W���_[
����,iF���=z��RIM�w,���>��|jC��������������<��=(RTqi�Tg��I���Z�9�^��N"�Y������!m2�CX�/;�&9����Y	�jC}��f��b�����#�O�G���)����	&���as�����X�7��4�3��&WU���
l��=[0�v7QxC]��W�-��nZ�z8�^�}��
CfLC�����D�"Z]�_T��<�<���'$��P9 ��S�Q��f���J���b��������Tg��=����f�>��\����T���Fz�z���j	G'r���Y�
��[1�{>lO�{,gJ^�M��0�\,q��,�m�w	`���]on�����~���m1����!�0�h������s�����5M#�w�_`R�h���b]�t���6Ws3���+��{2R������} R#�(�����`�0���
tg!'�S
}*T-i���J��`|������*q�r����l#;`:#�v9`��BM�y��7�Y��
���N}wZa���(Y���v]#�v�t�}����c������|\���%��=!2����:�`����n��x��\�����/�����kF4��	����T�,�;z����6�eM=:g�A�A�y�Q8+���Bd�"���ge������5m�~���O�Q���-��;5������^LK�&�+,`E#,�>?}:<���X�<J�����S8��o$;����_��l;?�m�c~�[���qg�6�Io����na6�q���N465/������0E����������D���\��d7���{�����U��� �t�H+$�����/����A`�p�8�X^e�B�C��}��������|���T;"@t?o��J>�x^	5��%�����j�E�Vy~������B�k�
�����k�^���M�n3���$dY�=�l��^w4��Q�)#�?�����}�FA8�
���6������Y#e��/���,��Y�#����&�Z� ��R��j�^��n�N7c�Y;������l��k�p<�9�
vz�8,���m�\��2+�x�q<k'�U/9��-�:�.om|��?���?v���J��=�(���\I���+�c�h<:3*o�ke���.������� `���P�D�CS�����0�tqAej�����=�mS*b�W���e��H�dmu��|�^��D�0����P�8������������w_O������;}�^3�4��&�8�j���ts�@��,�+"����Ul8W������!�G/�Mc���^�K��
_���v�u{4a��
EEl�����*�l��m9C�BF��2��!�Z���j0E=�����������#��%N����OM�A�t�������pt��"�}��c���x@��p�{�����SI����i��M�?���-�OO�����r�&�O��lK}�W��,8k���.1�F�rA��&�'�Go�T�>�9�&�Je��F�)����!�QO�x�np�.��k�Y�������d@1���������#���������p�<���/�l���J�7OI����/*����������]�Y�8!M�i�Q���,�$�d��{���Q����.�PNL�����S�����[�R�l#�T��&I�|��,���us��f������F	-��~�PqU����,K����v�3�t�'�Y+�/��\��p/#�gG�^���&���� T��Pw>c�k_�Rkf����9��E6�
���5����O�����9�$��p�������[���qsOcIy��a���+�|����i�0*:��Z��H$7���D'��m^b��/��w�I���
38ZX���"�&�������(�h� +�;�Y�O�[z%3�"��?��6��.�/�5]z��^����n6��-\������`J��"E������/p���'�#f_1B����_>~, ������E����k���IZ�6&u�:���5��
���
�B[
o-���!h���#�z�rIym�[y�O�:�����v��T�Kc��q�q����u}K��b����Q���~�o�iS�	�oI��P��$Y�=H�{�^����������oj�mt��q���w���2FDQWZ�[�J��:���1Mw2��Kx����"��u%��nOk�7n_��WQ;*�H�]i@XZ������Q\:�}8���}:�(��{�%��ps�����\n�q.����|\��y�:�VpCw-&#���F����b��|(Vo�2%!wp�[y�f1���[���E[����NcH�D���EC��l���+@�2u0���?��_NO����D6��g��]������F�1T�b�8�
�:R���
p��0xk:���~^FWZ>0��g�N�)!�hH�;��3\�o�q"\���*�eC�lmK�d��VX���H�)�Wb��X��iJ�_��4�w1�z���B���`���7`
7�i��
w��}0�]�w~Fin�{����z�;�������i��}��?<��t��%�15_�4���]���$W��g-�vC(�/[�U�V�?�����Fg������Q�������� ��Z�_��MS�W@j�X�����R����dl�S�-{���v=`�����N����l!��[�M��_��JB#�VCX"hdS����0Hrm]���`�a���h���Bsv�K#��w�*Em����I�:�L�+��BC��&����t���6
���M��8��w�40"Z�[�S��i�XP�3��fzZ��d���g)IyD~�awyBB`5�_�������)/���U���v�ha��o�;:��Ee�-R�S��jP��.�#�:���S�[�� IG�M��v�L���8��>j�law>��1���m�����}��c���'�[4�������������/$�Z��X�XS�y�V����K�F�(�PJ��T��L���0gV�-��xeD,���^(�P�r�Y4c���}�oV�t� ��|w�nl� ���$�'���>��D�����al��~��5u�Zpp���_�xk�u�&���R�P?-����n����^)���W!>��S���n1f��mV�mzy��4Y�a�������9�w�+f/�OG+���^]su�.BR���>O�����F�KH
�_���F��p���o������m��Y�H��xV���pq�hd��5�Q�u%:�,����/�R�D�����6V�C%U�p�4��������:�lF[3�_R��*V�bU�/�u%`�p���+����"Y�D����p^v%%X�DY#��1��6���z9\4M>rh�]�Y�a�qh��Y�����.�s�z+e��&+kSs�������65FN�����	8?oO/�?����������l���N���,���(�va��=���u�in���kwN�� �lO�x���D

�o>�_j����V�[�/��!�B���G�T2�?�����ea�W��4�\�����4b�f��{�������6#g�����V]�"m8\7�����{��������Eg�d�f�&�+.C��4i��n�^NOH=������s�!�Q���h���e��4��qc*#�<��rsx,��Z�2�y��T����vSg�`WH�G��^N�/X�>+�`�a���-a��F^ik�M��3�&SP�?}Z�m�D!
:�U
�@M�A�����Z���R�����e��s�������4�����.�����3XS�����d�K�I�8�`�c2[�_��
�.EQ��Io��*<_'m]<L�,n����QK�R�W���m�j�$�.�2��x��[����
����xX�����rK�l����x.�K�9N�.��5��-�{�����	Fc��[���������{�a��+�T�������0@F�!���}��G���%�4}���O�,��]����NI��(!y����9������o���# N�F������V;�m?\F�~>��J��2������Q��Ae;e������F��@r��2�9P�M}���-rI������T��LE.jY�4������z	�:?���L����0���rS����$�t�>>�I������Z�j�u�'��a�Sx!���o� �P��vy;���������O�Ts'3�3LHRl����<��U\��/��V�bb�����3	'�y�������r��������?GPl0G��"�~i9Gt	���G�n���g�MqH1��@*X��7�
d���7��8�#|Lp���,��.��6��|sz�FU�2#�g���d�CyG6���\�*+��y'�M�����Hx�w���G�h��\�6������s�l��c�x�]��>b�/���C7��3��D��/��ir����L+:$�\���	�����H"6!����G�)��w�m�g�U@�%X������2�L�����������@9�Xm'Z���,�f}�?a7t���6��������D+MS�ir�<�?n��?�(����A8��q������%�6����s�xX�9��JittL����9Z���!Y�S��@^]���Uj:��<��'0��O���?Q=����
-�nAJB6
������]���G	&������%��XK��2��Z+/�6����
�����[0
N�/����#XB�aZ�����0�ed�"�,�rA��T�������i�{(�k�>p�hk.�V��,�}d�J=��J)�?S�F	|��\���L���~�I���i�a�)K����+7^�IM��O�K�@w�(��L���t2�8��@l��7.��
?��p�o�hxsFz	c��`
Hu��g�MM��/��!oX[*��I�J�����?cN�Y����O�����-9M����f4�d�Y��8���J���w��g<-���o��k����;p:F�1��������Du��.n��#s�����e���.�Lt+zAv��
S=&�Efy
�����jq.��uE9�"7�
������lKhX�X�E�YK&�N_��4a@7y��@��8�Zr��c�����g�5,f��5|��W�H�8]�'q��������}��]y/X9��2K��'�oCh������&L��d��(���I,;_L���$hbSN7^���������oB��1f�2�C�d�?���p1�������9����
Xx ��C���sZd�9�|�������R�>y95��k���v[B]���[
�+��HV[���	�����G�����������/t��~����Q���:��hV�X�����:���U����.S�4�p4z��V8X_i!�/,�X_����������wt��.�7������`h�E����c��Z0xW�E7�LFc��p8�0<�������
+���W}X��[����M�����t�s�w��t�b������R��\�	�����y�����jM�n��f��YR
�J�KD$����).����@^9I�I6�AZ�D��3��1�Js���+�u)�^�|!����\����.�#m.G��1��[�oO����������P�>�5��Be@,k[�T��|�y���e���e��?4K>!�re�t���W����������X&o[7�����cR������WV��<7
�,�	���4�M�	?
`�:*C|���Gj �!�4$5��r���.�B`��YW�����SR9�Q����f���_F\�����_��f�r�(�1��c~��H��y�[���W�
]�������~d�9�I�Q�����Kot��F����W���9���m
�A|:�%��,`	E]����
}Td��4K�(��~�����?�:�x� 5�|+�<d������I�O����g������S/V�3�<M[���s`�5�lq�����:���@��"��SnZ�p=��	t0Ur�=;��4� MEV0I
Zv"K��b�"aF�`^����e�oW��hs�7�� �3��wVE�� �J�\�8r+=�������~��
H��O_�'(�%��ep�������Bb��<�ze�������BM�!P��P�p���]���jnl��p�7�����Gj�~�R�����@��0���N��k�iFu�x�*%6!�e�x��qDL���)��']��~.�;�F�� {+��P1�(���������K�f-A�)l?��3��3
��t�i�s��3��I�e�+^���0�������n���B8"�3�E5��c���S,�6�;��Q�&���S�6Sp%7�];H[r�,���6�����$�Sk$%����w_���a�y\��}<~�=��i�Y(�����0��_TB��l���l|:�_v����a]T^��*���+D]�C�dW^PD�����]W�d.����A�Z�����%����+<I��������e�s\���_B��%������v7F�T.���� ����|���~*��*v�W_���P>��E�>c����+�`���������<���	�.���>'��BC=�P{5K�5��k�t_����w�`��F��*/=_���5�a2��E2e�85�H��fW�k���*)jc�1>����x����u��"�H��lB%����������'Y��w�?[0M���9���}��[in�mm��h��mE��`��@X���\����0��PY�f3����*�P������Dl��/�������������)��T���_��$D�oQ`��������3"���O��������lpa
7)�#z�7B�z
O��$�������O�;��W�^}2�V��\b'�Q
�Tx��M	-u������NO_1�V�G�~�'�]/D��l���^���*�J����Dd��u�`P}�.������(!>��V�������~��-�U�+%�N�#	���Sh:a�
����:��+t$�.g�l������fo��tf8�����N��A����&>m���;��v4�><=}.([T������b��1��z$�p������������a�7�p��������xCb�-���q�&k��bO����v}�|[J��jx�i
�����U����1%Li=���?�����Yn"4H��c[0k�\��Y�4����q���W}	����I���dK�����-tY�qT�H�h)�������Rl������;BV�X�+ZF�9����*��OX�A��o��s�cA�Y�U�0'���1K��,�Y�/3�MN!8��C.�{Fj��SE��t�%��C�������q��V�
;����J����z�p�N���o�#�J�O�}F�:��68���q�v�di{4#�����?(}�]i5���$���B�SY\�<m�"!!C��-I�������5F(�F(�����z�snx����mp����,J+e*��%V���i�TN��B���$ey�hd��B�|xa��RsZ
�����*���W(� &�M���Q~�~����e{������� Y���P�e/�~�<�_7w�����Q9D9����rEo��q�������bv�JG��UT�������
G4�Yp��Ie��q���.TN��~=<�@���R`�F]Cg�>�s�M�q����y��,��*I���yb���A����R��
���i����nM���H����6���u+���UCm�����}|:|��i��yq.-�����������[p�e�_���<~p�H����]�r�0���&�L��D�WW
�@��M��T#���������t(���*��b����G���>f�nr����;�>Qt�K�"\��B7��n��w���tiD��PB������	�t!wH2��t��e
��������g�������kVO��V�+���Qb����U�jE�e���t���MC��|r���<�=�3(�����54`��Z-�`C;��N7��L��L|z��y{����?7�"���.���+�~q��l��W:aR_��x~�V��"�����M3h�����#�����J���v+���O��2�kj�U�>����a}�Sb�P��%X�����|%������J�NO�)��\��)�[y���J��`�0�o��j8�:E_��,Mn���.y�-�I��F�>i�~
��Qxo]A0�)sn���ZI[�'��.*��hVRm=D�O����O����5������<R��<l�7��LQ��ow�c�A�+<�q�:��{���t"+���?:
r�%����b*� 'b�_4� 1�m@<�
M�UX�I���h���{��'%�x_���]�D�p�.���46���]l��p�XL�t��Hg������@R5����h���	�X��R�`O�\����L�V���9wRDD�p��9��p*�K��
�)�ly�����v#���a�q��K�	���:�����������m�eD�YK�8�BQ��(~���'<��*���jL��y�uM����,�1cY���m����G������iR�y��>p�D}��yw����A-��(���TK�jZ%���P��8��&�*Q����y
����o�:
��Z��U�<^�&������������O4���(op���BT���:g�M�e����3�� �R�a���4�����(\T:�>�v7���F:��O�B��]Bc����)��]�_V4E�)M�Zg�$�j�.�����&8���y� j1�������qd&
*�I�T_g�$4�u�n�Uu�P��_��r����}��E�v��x;�l���6���b�kP��t�8��t��[���@�7����_�O��v�o�{�}+��f������w/�����H��������@�����a�8���VI"}t�$.���Dx'A���Uo*������xk`F:^�bshgt�{�G�	ip���~WB3��;��;�}������),��dM�a��Z���N0��H�y*
|�U�J�?�->�Kx��#;ja��[�	��]�He��M�[�����������a~a_Y;8O����U�h�aA��(��40��������O:tkd_$�<���P�29�:���B���v�!��SaVQ�`�k�����*�=���5�rD��U+>�Cb�����j�����Oa�#@PS<9%���++l:!������i����:�{e	W�JU���+�����~�PR4`Vp�����d�.����:����4�j3��;�*}naZg�k��<���*\�!Gh4`e�Mw�����������A���d(GW��4<K3�Z��N�K�6d!���-�xl���c�F�#K2�6��GA9{�^z+ �&Q��CO�Ll�uO���6�d��V0����[���~�������v�O��B��N����
�{q���w�&����&�+#/�(*��t�S�������xz�vO5�cK�RJ � ���U�3`�{�	���`�@�^,�hvf���5�r�'��<HcC�Q^��^���m;X�k]t��9a.\��q�=�g�m>�8�gh):��V��S�s��j�8����XG�9~&�$`�0@���������c�3��X������F���8��e��0��*j6�7���b>���R�W:�v*](�[2���* d���MK�V��^#�4Fb�
Z`�M���HY3QX��P�+>O��@s�y����W*�����s.� B�;T���V��������3Cf��P2������R�J��P���2���)+��l�NLsw7=�N�������F<J
+ez����UviH��se��x2
���}R��@����g.�Z���4�a�J&pb������w����y�.���j�a7������ ~SW�XO�[*�Ct�����&k}�hW����_�����Z��^���
x�����������+2�X#���Gc��?�((NV8ne�h�k-7L0�M?z����&�����h���l_���a�I�6n\�\7W�)���4��{+���e{�@Wl�������x�R�H@��������$;_;C���<�%(dx�L��uf��F�c#I��_	�����j���0������4��%L�^��j8��wnw�'c@O%5������u]8�n������]v8\�g����/����w��%��y�{�
��
>|�:?����7����9s+�a���������hv�Zb��+Oc�j�W�H���b���s.���:
���a|�Q��q�ug?���������I���j
;���P_����RL���!�?m��X����he^H�\�6J5��$�=��w,5�����q9�r8���2.�_��P�����/�D�
=Y[�-Qu����H������b��'��!��F~G/5��I:*�I��l�s#f���}-�+l=?g Vv��(q�a�V:Z���[�*X$�.9!��Q_����=��8�%�I��NI/�?X�:��#
�&�3�o�Zr�4����*My�=��]Ea�we�m�%����3�`�$��XP@�L��)V���s�"��BI/��7�I��U��<
���4��!{��n���]��Aj:�G��z�m�&���E�&T �/�uK��t��cNK����D/���=���"nn�TV�����R^���dMGi��)7�����R���#Tjov��o^����`����fO��~K���1��s
N�����-�z����7�
�����������4!��O	��#�93!�}��4*����%.�}�=��xx����!�<�z@V��Qm[V�(��-+��S^���|{������cG���Y���Q"��j^&�tQ~$����S5�<�t:�o�O����cPYO^�C6�-!��4(r�t4RK)�WW/!R/������Sk�B7��K1kU26,$~�������D�~�i^Ts�'<���4/C��K�����U6������&GXG�UR���z�V��=8�/��x��d�G��IX#��S�<�(�Zc������[��+����{�
Kv�K:]��[� ���:�	���f� @�F#��>V��?�����o�2��N5�U>��k��+D����B�:q���\�3X����HC��g�\�7�����G�3�3�x����ab�m��~5�U9#i�Vj�Y�����D��pUC����0�%Ln�%�Hz
)�.�eV�o��m�����(���EW�
��I�x+�X��=$�I��������$i�B�q��VNY�~�.�����0l�yXrY�S"�2�B^Uc��f����B�Z)��HW��3����d��RwTE�O���>��9�p�U9B�K�a;d�>�O�������Wr�3��C�����n���U"��{���Wu���8Ge�����B]�������<��Q�����T��������|��1"U��/���]
�[n��	x��iV�������`�����c{�{yp�����?��n�g�O�����L��g�����+	@g�.
��B�O��3E���T��Y�C��O�����1��t_�w���?��	���MzY{�d���`g�x��)/�<����
�<P�~|�Y@�1h���%M��;� �����
��j:��_��<1e���:
QS�X�f#4����IM�f�������q�u�q�\���p��Wv)Od:%�8[:�"����Q�������)*�O�����Z���@)j�����}1�������o�s�<(����%��g�3]�1AA%"G�Dw�$f�j����\Z��^V�^���+Y�(���������,�$��
`��������C�M]���{O-����_������i/�|��<?n!/����`�B6�t%�/��I�7n5����P��C���~Y�8*����KA���f��a��g���u�BB#V�B���+\K�u:�n�|:�6T����L���{���"�Al}-�E�p��g�����m�@����l5G[�nS+���7{4��%���@Qt-|b�����M�	�����nXj�#o)������Km��Z'T�]�>A?N�ER��=3��;�i��+4�����6j@��P��[������(~�Wi�2������zl}�E\[�P�F���7��{A��/_Vd�)���"�H>�>���?l�>��x�.�t;Y�\��`������b��z}z���A!Zo�wO�TK���?��_
T�����������g���/���[��k��_����{�"tm�x*1/��a�m�y�
[���Yu<E
�k�-���Q�rN��4��cq�z�M�cpM���������N�.��n�_���ESN��q���'�{K�
h����M0���nr�7>lF�9G�
y��L	r��A�Xg8�JXGg����"�|��W,�sB��f�nnG'���:8o�N�^E��y����W��sz7S,=61Tuq��M���\�����XD�]�]q`u����[�5�_��!��r����������:����	���>6�%b�iE���8��b�q�-�B�g
Q�b�1�P�,Ga����<�����P��Uo��+?��������PR>nO��'���As�:3)kv,T[�p�a��&WC@�y��i���J�S�����$'��^�����4���Q@�9*~�n`BHgj[�������|�[��nh�Y!��q��J���E��r��*�R�_��aE���8��Z��c��b>���-�(C�zV�K�1��vj]�A��x�����2����V1;���)1w�/���0��Qq���j�����x�����r�wyu�YX9�Y��W��yzz�tKr�T����X�c�����Z��Ng�y��������QR���N�o�+��sw�>��y���0TBK]�;S���H����N�[	���P\Cb�B���������v�����4e��6f������8����c��	+����'�.&D�`��
�&�w�w�m�8H+V�A�_8��=_����/l���N�4���r�Y���&�7{�����/����hB4r�x+�P/�(�E��EC��l�}8�PS���{e���MQ�2.�7.5e�m���~�}�������x��{8�h�D��>�@��<eq��(�����yx����	��i��}�p����?�[�*g~�l�o�<�����Q���u�/��0�z%k�E,RWp/dvU����t�&@��#5g�EM�MU��eQ�=��(X�1IV�9������E!�����n���yn5.�T�
[�+��w�*���V�P��^��x�s/
�VE�s�����T�rUf����!��L�T4���+A=�����Y�pbg"�*�];wO��Q��53fQ�mSV��q�|���#�6�P���
G����W�Rg}FSO'�`����0��[����v ���R��f�$��O	�r���x6�'"���������(	U�.W�����d�&F��P����is|\_���?��|����4��],���U�%3�B��~U2�G	�����A8��f���m���5|k������,���j��g�����������_7t�BEK��_�h��$e
p%�H�!P�<�l�����B*OpP���
��w�A���}��/�����V����w��m��u��35u2B�F;��ID���^K�+Q"7\;��EB ��0v��A���&�,a���_Cs�=�_�5�������
���/0_���j3^�5�4�y�U��,E��6��vutE��W���2�<:['�,f�I����f�kDp��]�;�P����76�����5��f��/���XC�6��t#;��O���//���S�t�������/�|	�ur������G��%L�����y1��R�����,$^d�Q:�����z�����gv�������`���a����&�����H,^���������_'�)S��9:PFV-�89p�Z�^��5��w�/�����2�a��QB��"����������G��* ~�@��	��-�<���_�-���i���oVC���cd�r
D���vy�
F��������i�%;�-�xcq���Y���,����W���2��Hhx�Z����\�@������Z��Q3]t��v��X)��8�vj��ol����ER�l��L��i�����?��h���z8�c�
w��9��JL!�ljy8t�M��m�������!p�m��������n>�},�<��M�U�m�[�U�'Fe���-ugJ�Q-�c1���1�#��Zkn�����q�g+\w�������@�QQ��HJ~M��o�]&�i7
MZ{��"\R�R����OjL~/smI��v��2Gdy%�� t+4���H�����v�zW���9c�2h��7���+A��,
������h[�����d������"����t>���)���z��d�L�}Ve��Xmu1���1�9}���c��/����Qwu]�Xp��Ge�A,���W����f-(c+���"z��%��N��Lb����������S�I��j���r����������\�6������.r}n��v��=nq���7�' ��)C���\sN+���.)��}���z���}��Ps����'V{�B��#�v����0l#]��X������Nx6��W�V��T^t ������]���cX��+F� K�:~�c�������#���"������
����Oe�
6��)'�mA�?�-m4TP1��^��A���k�k��p���
:Y�;�6 ������v'��jX���V�N6xb3Ba�v����_������SP��EE&��I�f2��.���;dl�c��h��������������)J�:V����b��F����X:d�� �g�f'Y�Jq��my��\y��5��t=�Ne����y�zW^���+.�J�8�,�c�W��	��'VI��6T�1k��]������B��a��GX>�i{�[W��~Y�[�e�	�r��[��:���D7�h��X��MLMvot����o��H��O[��4���`��vl
o��/��n�,�WB�ls"E���iFc�l��*�n������t$�����.|���o��8+����.i�b��M�����t��4��f9wN����j��h���n��+�h��V�d&��4��,5j��u�	+#K��D�"O#K������"�49:���[#�Mk�$������������WoO%�st
��G��>{�MB2��n��'�o��EG�Z @�&����w��C�[��
�?��:�GUG���K7��n]gXZ��)���p��_���Rh�tF�e:��,XA����_��Ls]��L�����w���vwG(������Nh/|�x�_M����'.kx����3�j_��4�5�M+�]��:!�G���J}���N�E���:�I�����,�4y���~8m��C67������xq�.����q�����
^�Y��!�\��]�s�����Z�FJ��a�9N�O'�/���6��B�w��^�	8�M�wP�[�e�_p��woM��5Br���0��i��?	���#`�2,T���X����)^�c���������i
=��*��$p��+�����F�s�Z:x
�+�\���eM��������;��7�O�8l���vr0�E.�9���<&5N��Ii�����/i�)<���	��E�*\�UZ8�0}�Fh�p���l.�>5�h��P��������5����%j��p����4p%�\���`:}d��]�;����a�h1��4\jL�x�tz�\�+�
�����,��#�tl/����b��,	
�Dju���ni�$	r�i�

L�wD����A�l��u�n�Q�MeE�������-�~�1��}?�9@���
]��U��K����/�x-J	��T��/J�����m$cb��9��-��vs2f�%b�2���\�aAtG3�:8l|�����B����z��es��fr&��,IB��D���S�__l�������f q�v�1��z[	�<���,z��l�}u�Z�|U_3%�{���������/���K�4�&]�����>V<����=�/�8�N*G��������������W�.-�yH��Z��H��������{�W��������L:��xc��
u��3�Rw���P��y
y�'��f-[kKk:��6���
�$��3�l|x�G�86�������(#��s��X�����Q���s����S�����`�JeL�J��F�N���V�<���xs+zw��+�K���zCt������y�b�Q�dIl._�n��2����A��j&;o\"9����-�d�X���+��.%Ar] ����@6�o�.����.��x���8�pm���G`�?�sb��W��B����3�h#��Rb�m��10���\�Z��m�z]3	����aw�~:��\
�4��<6�*6��Vt_
�h���y��5<���25��f���?��K���,�dRe�g���b!�Kg|�t�Y�i�����.
�*6���Q��*����/��c�G��+)���}6m�f(��/jHHJ�+��Pfj������$UA������B;�8��D���G�B^Sq�.��.���Q�����4e����M~=�,{mmQr�lep�E��'+�����S�nh���a�������$���+�L�0m��	�Rs~c���5dd���?U����������B�q8p$�8jcR����C����r��������n��mwB�"�
���"������d�`lP�0�������;'�\��F��`�5�]Nk��)q�lBh�
����{������|��|��A�9S5��<U:�1�����d����`�%�;���5�����Jq�YC�MxL�r���������v��/t�����Wu0�b�����+����^�.l���jL��������!0z������i<��<���*�����n�-Q^w->k��'*��h~a.Wo����44����C�~����w�����{��qQ�/����zt�I�;�0��s1������(�_������B�b.^�����.���},\��x���u�[�:�� s���f�,#��8\���
�U$�������#��E��8+Z/Q)�q4�Y�{�"
��>w�T*���zA���z�C�
�>$�Vi�nT�Bt��^�#tT�('Ur:����Dh���������0E����tj�����}��H�e����	�w��46����2`�qo��L�^@�0�����U��Q����h��.z�b�6�����Tw��6��q6��*
4��V#}�X�K��������32x��w���������d�Z���{����s	v���m�����<���O=�)�k
�vO�U$����C�K��`���o��'��6���/���0��"��G�'�����+�Wt������$"Y�%�-��He�0��%N���g���B�"f���@���.�x�w��r�wi*��r
��s�����R��p�������p'�HN��	0W�-&�������y�g�I	U��~pov�R�@]*J]�pB����O���V��;_x�#s���|Q�A�W{�Gq�����z��}+jA�^�~XW��n=�})��+�U�$f��|����,�C�C
�`��3gX�>�'N]��r�����*dmC��ptB��&�s5A0\������mU�!����~J��0��tP�BFw�����x����Pj���`89��M���6��n����J1��������v�����G^����C�I����
Z�f�^Wx�����'N��J$"]��,v���@l�/zC���d��hMF�sAsx���#�P�{�j����RGDM�G����A{�X7�n����f�@���6��C�A�*[�:��s{��_����������7���>F�M�V���w�NI����i��C�Od��4�%�1���Q�J�MP��4�(@��J(~CU5R��h@)E!N	��l���M�w�c`xK]��m�2R�{�E"�r���z��.g�*�v�)����K�%C��Z\��)!X�[���H�W*�<Jq��!^��u���c
W�
����K'����/����({���2UY�r��W+,�<��&�*��%M����0�~?m?|��*ab�$)4X�i���A^f�y��3���(����'�\�-Kz�l9��@��g����Lny��*Ot��|��������m��w?Q�7P�
g�X�����������kR�]	"�[$*GY�|��i���
R����.��V�H�j�v����qr�����8������E��}\���oq�&}�&n-M\��b����P���Te0Y���
��)o?�\�����hd��B��_U��������*9�:
�2��^���T/[�#�����MQB9��a�/#q��Yy_=���1�!�����������CI�G�
�gXSs5��X����Zgb����/6�}� �c������y5����"�#F�?9�'��s��F|����b����$k!�>�<,=��^��{�E#����~Q����}Ke���w����>���/p)�Xx����$a6���d�� C�Q�����=����^��hI��^H�pn|JBBDs�����Mm��GZ�S�97	Ot�!`��?JC���c��NMRb#x�������v�8 ��{����a�����n����t�����,�,�KI]��8�._����|�$a������z����5f$��k_ ��mxU���G�����@0�&����Z"�P��ss��K
YS�D�w1��X4�UY����\��04z��=+D���P������'ZY���w��@�/��o��*�b"���^�M��U���X�����������C��l.#{����BC��.B/I=R�N���V�L ���a�s+�f�CZ=���k����7+}wY���U"3�R�-����Q����R����5
x����H�D��G��`�����i�.Adq�1c��)4Y)��H����i)��C�8��\���������8���j����� �
���=.Q�����X���=���)�/(��[�:�U��F����r�Aa?�������k�pv�n#��-��k�i���l�����]����3�m��B g�t�T�������/dWW����.��+�=���e�T��J��+Dr�%%b��Sb%���y��y�DO0�KH�~�c��m-������y�e�^�d����������b���x*�ht���&���j� �E��Fr���e�������p�+0�
le����X�R��X��~R�_�c���V��5�q#sH����L�lw�k�@�X���u!���h�;P���P���]���o�"�
[����[�jW\��t��	@�}�P��s�4U�H�P]��������~���-�I[��I-�fF�)L1I���+�
�JX��2��.�U2M�VoF�2��v&��3�c���\��<��T���O���cg=�	��7``i�{��� [_\(6-�>z�ka�����-r#�*���fb����;������u�3��.]u�?�V����nk�B$��u�-1>��?�c����X���n�+C���B���&�S���,%�5Dy]���O�O]?E��/�S�`h������,����pS+#�Cx�	) -w`������3�d�RC
��0����K�8�BdA�$�v!��"�m��g��GW	5C��<+
XN?2=�T/T$����(���?o�aaFS��oe���[)��6n�@D�#�,�p�T�o��|(�R0t�M��������U�> �v�Mwp�%`��2��^�t�#�q+��1Tv~Jv�?�R/UX�OX��#:��NS7����+�|����
�����UT
��Pb}�*������"U�N�)"n�>[�Q8i��S�]����v�k��7��9�5�y�$��v�flta��1[�M��*OV��~��*iz������x�',�b�5�������Z)��`������]jR
Ml���Q��f'K�xv�6)!��MK��w���==�Y������I�����,��d��1����G������*����S��#�k>W������}��I��Iy�&E��������Y�5�KZ�)���8hHd���qS�.���k��"T���hG^���>��~L.e�#_�m�}���#���R]������pg�~r��B��Y6������4yl��]���F7�C�S�bJ���F(�XW� <G���)BByHS��$��v�?`�K���A1F�kc����9.����[7�rR����	>�t)��w�T.1/P�2�2g4}�h�����C�p�6���@�4����4�IW	�
�����4��tT�;3�5�Pv��TA*va����^Ek��>��R�`�&�LL�e�
��
��Y1�7�U��^*u��FB�2�4�p?���Z��uN|jn�f�-��bZ��pJ��e=h�!���	���u����y:��0Y��]�A�WY�[�@�\�l�w���Q��#�������e���O�;;,	C��?j��n�]�������6r7�V�Y1�]��w��Z�� ��+�d�1��������������t�Q����������w����& ��m_�0%og�
�/rQ4 r��=V�!���3��s�3<h@?5g���Tc+�����CR���W�0(�O�u��M�9�6!?���8����)�C�y��������_g��{j:~���o�h�xv�Yb��6M���������7�w4��>b5.�::1�/��&��|n���e�>f�����su�
EP�4O��lQ�y���L]���-���:�����.��$���P}��|<l`9�,��'�b�9�V���_6���m)�tM���W���s2m��"�{aT�v^�+t�1�!�N�+
����ZB(1������������/�	h��|��i1�6[v�rK��XB���al������T�T���y�[/aq�b��OW�!8J�j��.�N������=��l�o�w��3Awe�mR�<=���3ab��]\��=}��N��)���Aj�%T];�W�I���I:��	q�3�C��3�Vn��3�T��������#&Q=�WW��_�J����!e����)�z�f��|��p������������s�[P�E����_O76\���*R�b����m�*I��k�U���x�1m7�n~���b���b�fE��/_�6����a�q��_o���
I�.o�&���80/�F���SgYN��t����
x(
����o����-����������K��#���~v�
k����{7ve2O��4-�����dn�h�Jp�~^������M@
�'�g�	��	Y���q�&�����fw<l��x,l3�yuCM�������J��i��&YK��?&�]o$l)�r���F%�0t��������UL����^%������1�X�d�����@5�������_6/��Ej8<<���.���$B{6Zk:%^�iu��2�UPP+�fy2J[������a3�l3�c�O��
!5"����������,h�/v������~���Y��U�(�3C�Q��k�7�Z�+4�XN��<.�e$����&�4^�s���)�q���g�[rC�:v�lK�^�)��U����$�j���l�W�D�0kn�k����U���U)#�5_U�o��eF��>Mh_RdUE��X�wA���	oLKG_�fg����3���Z4����(�������D\M�;�k�\�j"-?��D\��Xad�p��2�P��^�-����Vy����t�I(��T�J6 
��A�NX/������8k
���;�n`�;#����G0���fV��c�(�ao�f�����O����X��f�Z��(HF�QO\���dj�x��y5Y��m���-,/���X��?�����uky������	=�;�vr�bG�����2c��F�L<�I�QU�����B��,QJc�2N���~9n@x��>=���qeJ���]�f���$Gi��K�	T�h�0G�Q���W���-����-�����e
��"I�J��mb;���_`�M�;���E:��"5&��I^�U���$�C�����.�IZ��w������7e�bc[��=��]��-��<vX���eC�F��p\�$�O�e7L�y7k\uh�%'Hb�'����E�ZoT�{d��������_o�|<�M���n�b�p�l64��%���&�6k�:s5�{�VS�F���������j,Mz�������M������������B0GD���mcor�1eTCnR��G�7���G����+@�O�o#�h�w���O)�������i�u*����@�L.+���K5W���X��.��i>���,k���LI:�h�LZ� �I����U�;��
<I'�����G�[�T�U,�����\6u�����g��H�
�0�����0�e{>��{����D]������IA�gYU^���#[�~.P�w��w��O3s�b�HoD/OD����.H��"7�^�� �����_�^����	/Rb\&���j�zM(��&����QB���{A�������\K@�����Y[KNs��Y�\���Go�KR�, i���u�����Nc�58���2,���������A�k��.����@�C8K��jq�])��tH�����Z�w��Wr�������p��/	���Xg�N75GM$�w,d&q]���u�u[ �AA���[TT��������tl`�:���/������y:<����a���#��o����O$6�*O���r��__>:�#���^AGC����6�����+�Fj9K���gF!�b�J���Qg
M��`�o/6�n\A9�z_�uQ�L�R�y�}F����B�O��L��g�G�,��w��KWs���x�����?H],�������K�������DFP7m:ld�_���)��WZ�et/�\����4�Y���z>{O�:��,�]���R[)�oL����&��tk'p<����{�e�����VW �#�r�>����Q�EW��?.
���j�6�Q���"���� ��[��\����w��ow�9�C�/���j�B^u���/�iu�=�w�"����|H�jS�yh�����3z���fG�
�Jy�^Dc�B��w�:������s�(y��2�M������Pz�����L�-����4,2F�Cs��5@�y�*b����${��
c��[����i���������i�k����=!s=���o������RJ���w�$s�y/�����5���.�T�F��=l�������%�����o�����k��
��X�����Zsb�_Q�E���X��r�����E�-����M�N�/���F�D���4b�Ah^9��3�Be���}yq)��-�!�kdLap��@Q\���bMt?e�5�&C�%���-��g���{���7ww����{�C`����l�B=�r��Gj8�#�(���hs���FY�>�K��O������f���l�~�s����r:m�OC��r��Md�<[=����]�L&��?�6���?����n��/���W��%*�@��-!t�<�W���K��w�����������T)�����R��'/��ubiL��6|���N�K����-���2����T3�Z�H4�1pY�i~��R����X������0IjE����rW���1/������[��I���=����������0\�&	�*'��(������A��i�����J�����J��g���?��9���\���^�����������r<�d��I��h~�K����p���a#�g5y8,D�����t�b���i���v��C�w��~Jk9�-W���
+|���B�U���0��������������7N��}���a��EnO,���������o'��e�6V�Y_��q=2�>n�o���~B�\r�Z#�J��Zj}"��J9V��6L!i��W�ZQ	�?��������k)a����1l�a,�����3�,g,qL7�$�Br����C6�������et=3�/�E��]Q������]��Cs1�_��[�IN�L���&B�e|��0��['���&��������@w[xa((����@�!/{������F��$�R��q���*�:T�G�r4[���M�.�U{�v�.� �*L������g8k/��ilg"K��#7�'3������$
{4�>�p{����P\�v"��uq:�NDv:2��t�w/��w�)�
!RY~���U�q�J.dV�]�x������� ���&��`���\���`&��]E�PJT�'J�bCj��
J��Q����]Q�CJ?;�C;.M�i~U���gw$��.f�%��|2�&����v_����n�1���������w!�����yt��LS9�������:��},?��u4v�f���&
m� �vw����E4j)�Y��z)��tk�e(��6��TY
������	���)�x��Z���B�8�Q�d��	0��.f!��B��������f?n��������d�v/��Q�M��v�l�=s�Fr��%�t%��������b����m��e)��Qv�tx:��>��y>����&�^m����eM�2�e�TJ��XC7Y���!��L���v�r2���{('��r�;g]wq���/+A���
#��Y�m���&��:�������n�}�^��z{�V�lg�l'Yot�Z���� ��'�`�(
}B�D��if���0W���8G`pW���;�72F��
z��5��7�7
�2:����j{'oQ�s[�����utMv������a����3�X� ��f�:k��)�������������2F��aoxP�No����J��c��,�����Z_5���f��[��5
�f�f�?[_��7�M�����Nx*���l_�d����g��2q!p[�~OF�i0�=�(������K�lW$)��)I��m���P���d=�8�]CQ)�NE���.��u�&M�j0�������`R�G�
�kA�����Mi�81�~G�{��r����e'�ys�)1T��,X��b�Wu���Eo�{Yc5B6���������o����UF��G��"��:�A�
����G��n���b��U%x�(s+�^���#�� ���bg��"��A�h����&f�6�9,�5�)���=E),�����gO3��x������:�vWz��.Q���>^�R��jY�����qs����������/`����*�66���gT��\?�P"��>�U��p�����������fG��nOG�p7D�T(�#�I�$w��1�4TJ����^c<n!�kw_��^�������g�n%y�Q����@G,a�"rxz,��0ZzA����5+^�X����i/V�Q�k���j��b�!2�m��z-e	�O�O�
!�����F�K_�nd���S<��J�9|K3����]W�s������d4���r�}��k��>������W�x��F���8�k����%tT<��qT=p�E�*�Y�F�i_%-�a3�����yh�)N���[X���5T���[�}�,�8�I�}J%�������"��I����n�[�@F����+����~z��[N��'��x�U��C�p#�h��Z�axx�+����@�b8�G2��+�It����X3�{��Y����WD��e�R��mv��p��Yj�������u���%��\x\t
a�D�5J��>X	M�J������V�{B~������������_�0)Y`�*v��W~����n����*s:����i)~-^��f^��5���5m�*�
!�����O/�����;>]��2h���i���n	
LbI���gU��~<��P����CQW�m���/�3�P�����X9wp'�k��t�:!���o�[�~����	��� ���x	}�	>�M�Ax�hH�"nd�!�3����1�yj�gG��(4#��RM<�����F:�����S�u�������X7�Y�i�J�U�������,��f����.=esq������.��q[���=��f{(qW�&
��u��sdhD�>d_���/��}l�b�aV��S�����J�nb.�����"��,�{�%�7
����E��y�0����3z���873i������'�b�'��#��b�Yr���6��T������C���R�X�������x��{8m�Y�C�c��Z���G��t�..)�����	*���-�#7��y*�����I�~���UO�*�a�s���y�����Ru��}L����64xA�UVo��b���2��}7�&&�����WS����0���l�wu�m�_e���x���q������h������*��t��H)���EC��t'?Y�������\Hxp��L�����06�������-�����������`:��z�6��S�����x	��S���?8~��n�&���O�&h�KJ�&��������3�j5���_p0-������$����.����Zt���=��{�]I6��l>�;���=�oKwh=A��Lc�����(��3�<{nt�#~{��������n��R����!��s�X"�>���3�����U�-���~Gs�P������`��W������j<L�:}���V���o���o�YtA,��sO�f[�&os�Fk1�������oo�)A�T�����{�o/\)�xi�C`��
�����r�@"�������'���r����gs���,��A��
[�����4M!�N���Q�\�����8	�}Y_���h��)X�������m�v���j����u���J��il��ZuO��W@��r4�sxw��Eta�_�4�Q�w�G����|�)7?�������PK�hn�9o�T��C�s�����)����/����v��l�q�'����Pcm5N���|�j,}N. �:���� tQs�����W���[���t}�p��7wK��.���v�B
@��U"Z����7����y��*t
�������IQYX�|�NE��5���
�+��C3)W���0���J}�#�k�������0����UJ��=n	r�1cq�C��{S�4fcF_�-�IyY�0���R��J��P�%�7T���V@��Yhww{�Y�!<�m��
4b����>���lJ��'���*�}�@���u��c1��q}x:����5Ox�35�-�Wu��\�1Q�
D����Yv5�R���Q��|��z1���p�"�+�!\�u_,q��z#��$�j����$2+�����.2�q����Vf`>b�c�Z���N,�Z�Tpv1���=+������nOg���a����2�gi$;4>��uZJ��S���{b>����2�B�����a�������M?;W��/�=�����[.�t�@�����1t)Y��d��W�������0�L+c��uk\� �7�:�6q������O��~���������DqY�����be������TX�_�-�|������f�"K�8#z�kw�{kr���� ^������Bj�#�^56V��)���(�od��Q������(jc����X�9r�ya�J�nZ`p+������4&`���|���$���zt��a`��U�T���ew��T����i�hi�O�������m����s������>"����:����/w���i���9iXj��<O�r^��(e��Lo|���yu��)�AH��,�a�a���O]���,Z�s�!�M��5��T��A����,)��vj����@�"��D��&��K���J����.��$��GG��r��������5a�3US�fdkFI]G.~�6X��h����Y����f49��W�^��D����p��	�]���W�[���RF�2��^J�ql���e��b$�lIn\���^Ulp���.Y��H\LBLmn�S/�.V�m�y�����bs,+��{��Gu��<�����%���=�F$�����a�g�����r��a�����X���&o�	�X�n���l+�b����2a����������8?��cI��!�zV�y���E����+��P��^����q��z�j�S��bw��m�"��/�W����N�������}�1:�������������2yw�o 	��r��8����i����/HlR��R������X��KC&~ED@����4����i"�4.�� g�.�b�9C��cl)M��bpa���`���������'�O��W�� ,�;vS��}����}��}�},���%*!V������%t[ZQ�u��"Q���:jDJ��r��L�����b�dAIx�Qb��P\� F+�n���������\���V�/4L@�-�����r����l�v��h26+r�����������p�|���)�M�v����%����`�-�����yoH� ��+�Zts��P�! �����(�&�U�3���Y�L�e[�#�YD)'���Y�f�B��������[���c�����f`���y����^��]UM��{���0!�Q�"E7m��U�LUx��W��v�(����^�~�
����&��^`�&��hV�k��#�&7��`�k`�/����=9wMRA�	BV��&Q�/����]K'��%�D8���s :�#��c��W�
s�������&�K	F�g��.v2yf5�f����dy��M�V2��������<����7i\���m��Ta�xt:�����Wdd�Hi��^�jAj�,�H-d=�?�;`AOc�;���������B����IjRl��,��]1���.��ibl��g^�L��"�q���Q��~���g��6����0Xr�����$M�`������oY�?�l*F�~W�_��qs��*�0�X�up
�O�oL3s��XX�����v�,�3�H��+��#t���<PX
8����
�i��e��y����0�>x��%=DOe�^p��	���5s��%�G����~��G���u���s?������.�-2wh���?���5�.��ra��38�E�+���P��wh�=�@�,�y,�z�{��8��%	��u�L���*�9QB^{UHs�U1v��w�k�4������iG�m
�p���]�~�b�����)�~n���<�O�i��x�����s�^�����������1��u��������aw{7�u��h����C�F��]�]���F��8�+k��J���������_����~
�L����D�f+
�,D������<##%
���nX�d����~��`��p(�~�F��_����0:u���E���nF��z�������b��'k����d#���.s
5[��>���q���uV�:����������� ����\��%����wKX
�^~^������'��y5Y�����q�������c�T�������1N�,h�HQ������og����AL������jF���KN�!��Y?�R��
O�K�5���l�D�8������A��������+��A���2BK�_����V\J/��a����$�����_
�v/D����(�M���
�����
�gr�������_�z
�^�E�f���]/�@(����6(0�[[����D���s�n,]{/D$h��>���d��aw($i��8�Hh&����*�	
�V�Wp�@�z�F/�8f�����(��b���,������������t��<�L��,��ls	3%�/a�n��(�
j-n�j=hJ�P9���y-~Z"����%��
CT��5Q��$U�W����������j{M]�^�er+l!�fJn6�c��w��F����/������@�@��D�<wZ�����%�$?��p>���4���_�%��2�S6��
��-�� aY���{A����v���b8�S�|,��rX.�04�S�}J��0����~�?����v�	�!���G�fKs����X)w�ih@64l�(����� y�������/T�����T�6��(w_�i��iW!��pS����L�-��K��<��<����;S4�ro�%+�����E�<�a�v�z9a��O���<A_�����hc��[]M�j���#�j���B�L=�k!j6L��!�}�D��>Ld">��*�tH*	��Lm���Y2� 05U=���v������>��_�|$�E����[��*���PJ�S�wO��w4i����:!����1u*r�|���jF����:j��-;��gH��(����
�^Q������;(�^M]��9\�67�n���(L��+]�1��G�&Ht��� ��� �uQr�E�� $�m���t*�Y�����5O�Ru�^���	S`�mp�F�.�px�Bs$=��N�x^)p%2�(�O�EN�?A��5kL��dZ�6���D��>{�;����d0�b�.�YX)"T�fo�KYq��*���������U5g�2;U�6U�Z�A{�aA4X%�������!#�k��kY���dXX�LH�K�F�X�����sh�$oB=����G������LV��e��K��n<B��-RJ+���#�v�f�N�h�y*)7��T	�����R%T1��B!�a���#�B��U�S�K���Q�Lh��K�s��kJ���
�972����z��-�<���x�6-8�i0��F���H�$��NF�e��n��B|���(���ru��v}dd4����u,l��:?�u��'���cM$����M����J-Q�&��.�X�9���� K:����IuY+�:,�����u�t,�~���5��0��������y����C��B��,��w��o���&Cl��Y�K%�zw�����-�ru�C;`L�/�t��].>��������'{��=���V���>��Z]Xy��.[��/�W�pCE*z�j�����UTE�`�txMs]����������==N�\���� ��&�|���P|�M�+��B��[�2���3t�Q���_/�������q��DH���S���K�2\E�=e���+�;	Gn@^H]������������W�����v<��������u���_�C��vo?� �z*V�I������n�
sr��:�����K#�6����93k]w^�&�Q��iu?���������a���#[E�z�K;���QH�fa)���v8(���:|�z���d�E��.�eVC!�x
M+&SI-KHW�@���n����F�;|�8-�-D	�Rmu��g��#���n��!����wz���50�;�n��kr"B	���
S�U�����t�V
y��ox�D�_nc�
{�@��m���}�<��r����4�)����m��&)�7t���c��>���=42=n�����x�����-�����>�?�v�8wt�6X$��s���(X��*�Nba�I��@�Pa�w�e��A����{9�^aWI�Mc+����pF3i�Y4�"6�z������>�f�g��-bDe����<��^P��������Y_��R)������~���I�#;����]����c?��Z������0���0�8��������UaF�����V���4iw�*���!W��R���6�
m���
ea��R��	�����~}��?R�{�4�IY�]��=������e�A�8�Yy�Q���ns�<<}�ns�i��-6�bz�C�;�t1���=V=f��H�qQ
%C!�P��~w�mhvz�\>20S�Z%��0��^��>2a%�4N:�s��$��9���*��]���*�bYeb6��8Mc��RV�N����i��N����/aO�k(�w���Y������7xN(��sD��r�i�o�����
L��YK
2�;���)O^��o����KDBq��.�K7�����K��CT�N�[��B�OB�/7f��B��e�!L��<5�B�6 �����g"`��e����
It&?n(U�2��w �A�����e��8Q���(���U<z�Ep�.��U�]�o�����Gz??<!�2�<�ng���&&5�ID����������-�����r1�����7!8�h�`>'��F�o���a��h���0�&�IC}�����K�T�I,~v6��G--���aN��0�j#�P����E^w��I/y�"���c�eg���/�i�A�(�5������>�	����0����������|�G���O��:����g�.��s������/��������,`�Nx{��f� y��}S��MESs�L|��#��EI`%6��M�����`w���b^	�@l.���U��dI��0'��Ac-$���i��0�7�4)����t���r6���]3��$�|�I�6��S�E�)K_UJY_����]2}���7��e\E��`�3��D����V�����|�#��M����������^`QlP����R��NS�:w�]�d��(�[���%L&K�|���Gr�nBX7��uj����^7w�c��d��%�n�Y��V5?m��c7��F.��<��Z�qCf�3���GIP8: "(/Hc���+B��v��2��~���H=��?<�V�y���'����1M��.�Ed`�8�{(&���������B�RB���G��.�m��*D^sLCY�n��h@�y�#P[	���yo"�C�*&a\�L����2o�D'9����T=�h�S���;X�k�[y�� ����,�^�A��;�B�b"�<>���[������
^D��H�
l���kg�	.�Pr9L�A�U�8�Ug^�x�~B57�
7�U.�
_��$��}���aTk���w���)$!+Z.�[�;��G�{xBF[�N�{���o�}�����xz><=�;}j,W*��p��o;
��#��C��z�>��5����*rU��7�?����_����{\�`���2�'��b��_�����bq����j){s�X������pF���8�d��u�X�����	����g������z��?#C�*�gT��z���'P���1h�����T���������)Vb�R��*`�X�}J�K���HcL��0��-}je��y��_�����e���e�$p�
3��w���49����:��:8�Vx����OC���w:��eL�����E���;��=���9Vi� x�FD�J���+?����nm�	��gM(���5
�V��������4K��/��A����G���N�����n�{k�&�J�M%��K�f8�;����l8�������i�d�/��
�������O��f����u�V|�m6G�����@ V�jU��no�*���,�B��eG�VN|�zP������Q��W�/N:���[!�w�&K�����$C�j<���_lyH��|�8��0��\�2�A��c�=I�*�U�]����R(�%E��h�-����:-��X����1S�+��U#+�(.�P|0G�#���0�-��L���kj����'�w�����U��hD��.���_Y�C��z>�f�y�����K^�9��u�"�xx%P�(35]U9�b or�p����S�[�[.50�����
=���f�h[��OcG��
a�>��<,��M��O�a=�O��P��z3�,�����~�N&X���I��	����<,���� G�$.NN"�u�8�7��$���������?�AW��b���40�fvR���d�Hq��
*��7����������*������v[
a�^C��1����f�����i0t�;�l�����z��7Lpw�4�Y����kR��y�q�<#895��N@8�_�
�t�����;�����s4s�m��
S?G^�a*�!E�*� M�`�	tf���1��AHb��Q�5�C�J����ey����+h0_�sod|.O���n%�����(�'�s+]��c�/<���f"�a�j����������t	y����l�>����L��bL-qt-��5St�7��&l�Yo���7���OS���xc�@yZ<Jj2����\D��At��HL��Y�u{�q2$�Id~�$��B��� |��$�J|r��u
0G����j|�|%Oz�z���kO�n�K�������D�c{?x��@�����~���"�pX���(�C�y���t<}<l���zrT�J�r�fD6���l���~D6r�
��3�4,:�ts��������P�R�`z������$Gv�-TH#_���1`/(�~�f���X���sXE��� 6��@Xj����;���@p���KXel��s�m���@8�{�f>L�YZ�+�DN���)�����xe�5p2!`��b��?4������g���J�}��X ��pq�TG^Z�9��P��QS��S12������^�h��b�
�Y����"cP���wO���2�������jc+��D�����3{G5�g��kC��g��`m����nA��:���<��z���a��@�Qb��Q0�Z��I�Bm�����(�������c���aq���<H[r�<4��9�<@�'��J�\��L�*��G�����w�fK�u�i��%���)��R�di�6��s8~HA��������v�.�������!-]\�FXz7����BD��4���;�#�p?5�Uf�.������v����y����9}B �H�i�@�hj�8�L�:N���kO"�R������W�����'��E>�p�]�����>�u�&�'��S���U�JP�]]�IB�T��2y8���%��D������HC5 R�.��A&N?�O�^�y��x�-�T����e��iVy���r�Zo�@�8��a���R�[�n���y#b��|cS��l�)]5�e�4}����3]W����=�GO�$��|<S� KC���(C�Q�P��X?�E*�O[:���C��#K�*�r�<b�}�e7L��%��g��K/�NK��������KJ������:���	�pBhzn�	''��R�E�����*��8_mp9��B
!���S���-(�y�������>��(�������,zr����3�\�{�E�v����DD����\��M�+�k�*���<?��:�'q�W�w�$��3����������K�a����yl�Av���j�{���Y7�;��5�������d`u���=����tC����aW�DZ6���v�����i�c�}���	�'%�'7�x��4�A�(�X�Z~U�@oC��cK��i`��n���*���2�}�S��[�^S��w�-5��^�@�V3$�!�����������Y����s���p|g\;��3,��t6v�h�P�Ho�k����6�'}�n����2���N:�o����\��jk�X�r�]�u�v����"���O*	�m���%@$z��1Or��&��pY�jF�p�s%��jA.����C������B�#%����
�!~����0�e�g��a�*�IT������6�w|�9]1��O/w���4)������C7{<m���>r�P�Y���,�)]�S�{=	}��v7F��i"U��E(�y�U4�6���*���?P-��tqt�R�u�W#��r�iStO��9�0N�a^�B�rls�vyK1Lt|����G����S�����{^[��4����wi�<kha�� 4��^V���6i�����/�4
2�>��Y���Fo��a���!�.��-�y�>��{��i9.�j�,���g����O����3|] �.�9��7`�����*LQ����2|�+v�C��@`)�o_G����k������������08s���_�'���m{�+��0J�~95��l�������6T�Ap�� aqu�����y(����v�S�$�@_������o_���1v��jK�f��(]�L�%��+�TE�l�f�F!����S���������k[(�#�v}�<��g�_6���ok@���i�='0}��:5'�R�$��hh*�BG���/���<����	1F���r�MW��5������J���x����Fw�!�9���N�k�r�P�)0L<���=A�Y�S�/���@��	�,\�������e�F��S�<b�����n�-�90g;KP�t_A�
�����	��u
��+� 9�ya�e�|��~�7N���J���,L�;Vl�"D��dY���@p<<�A�T^BtlV��-^P�kcs�6����W��ts��u��nk������fx����b����VK.\�����pd�M�^�n��6s

��q�����,�]��	�����^��!��n\��BX��{�*�V���4�+����zTu�UJ��oe�~G���Jsu>���������v�W2EW���� ������)`��m�!^��7�h��Y�7K�] ���J���}�=�C��?��pb������h��5s���L���Yg7��^��z>��(�N�����w�A������j-5/���j?�^yJ��a��cc[�����M�m�S��E_2��@(����C8%	TH����.)@���[���e)������p����o���Z�����xw������Rd������VW�����t"oR�u0M����$3��J�
S�.
ri�����@7l��-O�;�B�\)��I�����9��3!�6S;�X<����������F�jF����30C[r����':�~U�Y�+�������l�y���a��f*A��:7�[,��Y�����(pd\�����9��_Z�6U���+w���N��7l��T����oFs4����T�u�ni���|��`����L�D��[qD-�F��*�h&7b�s���V+e���b��6V5�?��k��k��������r����~�?�9��R���;.F��uT�����B������!����b����MT�V��~Mh+$�tq*���q�<��}�nqB�
���o
�/���i���r+0t?�KfZe��jrc'�ZN�U��Y��8]�#������U����?��<
��]����ah@n�[�� X�~��G*�`�|F�)z�:CU��t��jh����Ch�"����.���]��,�����sO���{�u�UT��j�to�%��+yd���u���i����w:���bA�t�B �W�$��,���q"0����htMt�+���qcIF�>���
>W��e#=��A�;�9�z:�_���/��eE�t��F;!�;Z�?Z����t�
���a^���r�!�b�K��SQ��y��F�9���>��T�������_F)c\��6���$.�E�x`�������
������������]	���1����I��� ��/�N�%n�\
	�J�������O����;F3�>�o����N��''v�1����X���Nl5�P��PE�56��de���j
��%y]7��uh��{��&=�q�w�z\��>�N�_���m���;l���j9� ��4�&t����
#�x�q
��<
?WlWn�&�<+
oed�;jx�<��������O�oC�H�ID����`����s|��M������c_�
�!�D�y��5��4
���cX\��[ve?� ~�=��Uv���� ��h��f��.C,-��p����]���N�x��/�]�Z����k\'k~�zmQ8�_g��J�2��7][�<Wy�h3�urq��C�x|�(�'~��0zD���k�����qL%�?|��\�i{��l<�<�����b�op�)�(���,�������Ub9<�a���B�8dz�&/�!v�n"��,���aEX���d**�V���n��Ah�>4��W���B���}���t��'�l��:���/���e��>�P_�s�hb���z�!�!KV�b� �x/���EL���Ph��	PE!n���,]�v���O�7g��z�� ������.�_��o8���wN~Q-} }��t��F)]���*;����_����>0���=�^F��~�.3���4�mg���@�L�x�u�]�a����|V����q3N�C���������k�)�@�����A�H��1�I__���FB����4����8>e3"�m:C�
+%7�m��v��]0�8�G��(K�0�]��d���fekum�&�"RuF�����"�����V�?t�pnPm��?��j��~RglP�S��6"���+t�.YFBE.�J���[@�b�� 
��|�p���`X@g��t%&���V���3������<�.�F��BB
Vn#3�i^0�����V]V6�N&�h��xw*LctX/�8!�1,��\S��C ���h�E���>��|��.A,A��a��A�dQ����U�z��a�z���|�J�a���<�>���W�GZy��R�Z�$������rY��������d�h��[�V�������lE�_iT���|aT�z!Q��R��Bd���h%[��{��T4���m<�R��i �]�
�{t�hi�]��2p�v�+�N�T
��4%�.?���2j��&�]7�������L��1j�g��������	RCX�vo]e�����)O��.<���+�<��W�+���������w=V'8�_�X�}A�
R����h0�$6���N����5��kh�En)���~���8������E�/W�Z�n���hu"�q��v5��� m^�'�qx:��v��d����,�nL3��F\n��=,�^���o�M�;��B�������3*	Q��\#1��4M��a�J^���\��L��G������a�WhKt�w�������Y	��&4�P������{����x b�-������CS2|���Y	U��c[��x�mv��]}���F���y�e��WC�}�����ua>BueE�|�\,�;�����d ��3?�10g^�8;�#��w��k�G1��C��r|H��v��M`Y�ev�f��5��w����>��5�5��������o���\}]���Ec{<�D���5;��F��%�)��
�A�J�E��a���f<0���#�gux{��v
b%�z}��!4�]�)�����$�i���!��(>��n���<�2V���@e�#��6����3@`����9��>����w�Q��O���NK�]��1�4�����r�F%����>�[Q�E��#�U���Wz���x�X�W`aN�F�z�y��8���X|��Kta��FN����}��}GOd�{zx(>�����LlD�/���`AG:`�����X���yP?B�J�l�<�����Lt�+��R���o������C:6����r���d�7�9Y6���.=�]vq|�
ej9��8�M~��4v?g����o]i8n���F-B�c��^Gtw����`�w�mw��t�;6%Q���ge�@c�l�wP�Rf)p.�1��%M.���l���,��}3��;�@�M����H�`���L�
V�@zY��@U����DKY����Ag��"����4�E!^Kv����!��IXV��c#���kr�������or.��@yP�|�hM+��9�
���t�0o]�m$
��\����$s��Lg��@z.3����_6�s_?���4r�y)�+���_��h�9��5���~��6������C����������{b�S��Vj�A�e�~����g�HM��x�����j�]���I%��
�l[���CS
�������+����~C��L�8�*
���u�\L��l�-��:')��_��!��H��l���_�����EG2	
In reply to: Peter Geoghegan (#1)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 9, 2025 at 2:04 PM Tomas Vondra <tomas@vondra.me> wrote:

Yes, I'm sure it's doing index only scan

Looks that way, from the pair of flame graphs you sent. Thanks for that.

did you update "bid" or did
you leave it as generated by "pgbench -i"?.

I didn't bother with updating, or running VACUUM FULL. I did run
VACUUM ANALYZE, though (can confirm no heap accesses for the
index-only scans).

In fact, all of the malloc() calls seem to happen in AllocSetAllocLarge,
which matches the guess that something tripped over allocChunkLimit. Not
sure what, though.

While there are way too many AllocSetAllocLarge calls here, I don't
think that that can be blamed on the skip scan work. Note that commit
92fe23d9 didn't even touch the BTScanOpaqueData struct. Its size did
change a bit, in other nearby commits, but it was already so large
that I don't think that it could matter here. Besides, you said that
the problem clearly starts in commit 92fe23d9.

The AllocSetAllocLarge calls that I see from gdb look like the slow
ones from your flame graph. They're for the BTScanOpaqueData
allocation, and for the BLCKSZ * 2 used by index-only scans (though
not index scans). These allocations happen once per rescan/index scan.
So, again, too many large allocations take place here, but it doesn't
look like anything that can be attributed to skip scan.

The difference shown by your flame graph is absolutely enormous --
that's *very* surprising to me. btbeginscan and btrescan go from being
microscopic to being very prominent. But skip scan simply didn't touch
either function, at all, directly or indirectly. And neither function
has really changed in any significant way in recent years. So right
now I'm completely stumped.

--
Peter Geoghegan

#129Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Peter Geoghegan (#128)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, 9 May 2025 at 20:38, Peter Geoghegan <pg@bowt.ie> wrote:

On Fri, May 9, 2025 at 2:04 PM Tomas Vondra <tomas@vondra.me> wrote:

Yes, I'm sure it's doing index only scan

Looks that way, from the pair of flame graphs you sent. Thanks for that.

did you update "bid" or did
you leave it as generated by "pgbench -i"?.

I didn't bother with updating, or running VACUUM FULL. I did run
VACUUM ANALYZE, though (can confirm no heap accesses for the
index-only scans).

In fact, all of the malloc() calls seem to happen in AllocSetAllocLarge,
which matches the guess that something tripped over allocChunkLimit. Not
sure what, though.

The difference shown by your flame graph is absolutely enormous --
that's *very* surprising to me. btbeginscan and btrescan go from being
microscopic to being very prominent. But skip scan simply didn't touch
either function, at all, directly or indirectly. And neither function
has really changed in any significant way in recent years. So right
now I'm completely stumped.

I see some 60.5% of the samples under PostgresMain (35% overall) in
the "bad" flamegraph have asm_exc_page_fault on the stack, indicating
the backend(s) are hit with a torrent of continued page faults.
Notably, this is not just in btree code: ExecInitIndexOnlyScan's
components (ExecAssignExprContext,
ExecConditionalAssignProjectionInfo, ExecIndexBuildScanKeys,
ExecInitQual, etc.) are also very much affected, and none of those
call into index code. Notably, this is before any btree code is
executed in the query.

In the "good" version, asm_exc_page_fault does not show up, at all;
nor does sysmalloc.

@Tomas
Given the impact of MALLOC_TOP_PAD_, have you tested with other values
of MALLOC_TOP_PAD_?

Also, have you checked the memory usage of the benchmarked backends
before and after 92fe23d93aa, e.g. by dumping
pg_backend_memory_contexts after preparing and executing the sample
query, or through pg_get_process_memory_contexts() from another
backend?

Kind regards,

Matthias van de Meent

#130Tomas Vondra
tomas@vondra.me
In reply to: Matthias van de Meent (#129)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 5/9/25 23:30, Matthias van de Meent wrote:

...

The difference shown by your flame graph is absolutely enormous --
that's *very* surprising to me. btbeginscan and btrescan go from being
microscopic to being very prominent. But skip scan simply didn't touch
either function, at all, directly or indirectly. And neither function
has really changed in any significant way in recent years. So right
now I'm completely stumped.

I see some 60.5% of the samples under PostgresMain (35% overall) in
the "bad" flamegraph have asm_exc_page_fault on the stack, indicating
the backend(s) are hit with a torrent of continued page faults.
Notably, this is not just in btree code: ExecInitIndexOnlyScan's
components (ExecAssignExprContext,
ExecConditionalAssignProjectionInfo, ExecIndexBuildScanKeys,
ExecInitQual, etc.) are also very much affected, and none of those
call into index code. Notably, this is before any btree code is
executed in the query.

In the "good" version, asm_exc_page_fault does not show up, at all;
nor does sysmalloc.

Yes. Have you tried reproducing the issue? It'd be good if someone else
reproduced this independently, to confirm I'm not hallucinating.

@Tomas
Given the impact of MALLOC_TOP_PAD_, have you tested with other values
of MALLOC_TOP_PAD_?

I tried, and it seems 4MB is sufficient for the overhead to disappear.
Perhaps some other mallopt parameters would help too, but my point was
merely to demonstrate this is malloc-related.

Also, have you checked the memory usage of the benchmarked backends
before and after 92fe23d93aa, e.g. by dumping
pg_backend_memory_contexts after preparing and executing the sample
query, or through pg_get_process_memory_contexts() from another
backend?

I haven't noticed any elevated memory usage in top, but the queries are
very short, so I'm not sure how reliable that is. But if adding 4MB is
enough to make this go away, I doubt I'd notice a difference.

regards

--
Tomas Vondra

#131Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Tomas Vondra (#130)
2 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Sat, 10 May 2025 at 00:54, Tomas Vondra <tomas@vondra.me> wrote:

On 5/9/25 23:30, Matthias van de Meent wrote:

...

The difference shown by your flame graph is absolutely enormous --
that's *very* surprising to me. btbeginscan and btrescan go from being
microscopic to being very prominent. But skip scan simply didn't touch
either function, at all, directly or indirectly. And neither function
has really changed in any significant way in recent years. So right
now I'm completely stumped.

I see some 60.5% of the samples under PostgresMain (35% overall) in
the "bad" flamegraph have asm_exc_page_fault on the stack, indicating
the backend(s) are hit with a torrent of continued page faults.
Notably, this is not just in btree code: ExecInitIndexOnlyScan's
components (ExecAssignExprContext,
ExecConditionalAssignProjectionInfo, ExecIndexBuildScanKeys,
ExecInitQual, etc.) are also very much affected, and none of those
call into index code. Notably, this is before any btree code is
executed in the query.

In the "good" version, asm_exc_page_fault does not show up, at all;
nor does sysmalloc.

Yes. Have you tried reproducing the issue? It'd be good if someone else
reproduced this independently, to confirm I'm not hallucinating.

@Tomas
Given the impact of MALLOC_TOP_PAD_, have you tested with other values
of MALLOC_TOP_PAD_?

I tried, and it seems 4MB is sufficient for the overhead to disappear.
Perhaps some other mallopt parameters would help too, but my point was
merely to demonstrate this is malloc-related.

Also, have you checked the memory usage of the benchmarked backends
before and after 92fe23d93aa, e.g. by dumping
pg_backend_memory_contexts after preparing and executing the sample
query, or through pg_get_process_memory_contexts() from another
backend?

I haven't noticed any elevated memory usage in top, but the queries are
very short, so I'm not sure how reliable that is. But if adding 4MB is
enough to make this go away, I doubt I'd notice a difference.

I think I may have it down, based on memory context checks and some
introspection. It's a bit of a ramble, with garden path sentences, and
some data tables to back it up:

Up to PG17, and 3ba2cdaa454, the size of data allocated in "index
info" was just enough for a good portion of our indexes to only
require one memory context block.
With the increased size of btree's per-attribute amsupportinfo, the
requirements for even a single index attribute won't fit in this first
block, requiring at least a second mctx block. As each mctx block for
"index info" is at least 1KiB large, this adds at least 30KiB of
additional memory.

See the table below for an example btree index with one column:

| type (PG17) | size | alignment | size bucket | total + chunkhdr
| remaining | mctx blocks |
|-----------------|-------|-----------|-------------|------------------|-----------|-------------|
| AllocSetContext | 200 B | 0 B | n/a | 200 B
| 824 B | 1 |
| Chunk hdr | 8 B | 0 B | n/a | 8 B
| 816 B | 1 |
| IndexAmRoutine | 248 B | 0 B | 256 B | 264 B
| 552 B | 1 |
| rd_opfamily | 4 B | 4 B | 8 B | 16 B
| 536 B | 1 |
| rd_opcintype | 4 B | 4 B | 8 B | 16 B
| 520 B | 1 |
| rd_support | 4 B | 4 B | 8 B | 16 B
| 504 B | 1 |
| rd_supportinfo | 240 B | 0 B | 256 B | 264 B
| 240 B | 1 |
| rd_indcollation | 4 B | 4 B | 8 B | 16 B
| 224 B | 1 |
| rd_indoption | 2 B | 6 B | 8 B | 16 B
| 206 B | 1 |

| type (skips) | size | alignment | size bucket | total + chunkhdr
| remaining | mctx blocks |
|-----------------|-------|-----------|-------------|------------------|-----------|-------------|
| AllocSetContext | 200 B | 0 B | n/a | 200 B
| 824 B | 1 |
| Block hdr | 8 B | 0 B | n/a | 8 B
| 816 B | 1 |
| IndexAmRoutine | 248 B | 0 B | 256 B | 264 B
| 552 B | 1 |
| rd_opfamily | 4 B | 4 B | 8 B | 16 B
| 536 B | 1 |
| rd_opcintype | 4 B | 4 B | 8 B | 16 B
| 520 B | 1 |
| rd_support | 4 B | 4 B | 8 B | 16 B
| 504 B | 1 |
| Block hdr | 8 B | 0 B | n/a | 8 B
| 1016 B | 2 |
| rd_supportinfo | 288 B | 0 B | 512 B | 520 B
| 496 B | 2 |
| rd_indcollation | 4 B | 4 B | 8 B | 16 B
| 224 B | 1 |
| rd_indoption | 2 B | 6 B | 8 B | 16 B
| 206 B | 1 |

Note that there's a new block required to fit rd_supportinfo because
it wouldn't fit in the first, due to AllocSet's bucketing the
allocation into a larger chunk.

If you check each backend's memory statistics for index info memory
contexts [0]select count(*), total_bytes, sum(total_bytes) as "combined size" from pg_backend_memory_contexts WHERE name = 'index info' group by rollup (2);, you'll notice this too:

Master (with skip)
count (d73d4cfd) | total_bytes | combined_size
------------------+-------------+---------------
87 | | 215808
50 | 2048 | 102400
1 | 2240 | 2240
33 | 3072 | 101376
3 | 3264 | 9792

(commit before skip)
count (3ba2cdaa) | total_bytes | combined_size
------------------+-------------+---------------
87 | | 157696
35 | 1024 | 35840
37 | 2048 | 75776
15 | 3072 | 46080

This shows we're using 56KiB more than before.
I'm not quite sure yet where the memfault overhead is introduced, but
I do think this is heavy smoke, and closer to the fire.

I've attached a patch that makes IndexAmRoutine a static const*,
removing it from rd_indexcxt, and returning some of the index ctx
memory usage to normal:

count (patch 1) | total_bytes | combined_size
-----------------+-------------+---------------
87 | | 171776
10 | 2048 | 20480
40 | 1024 | 40960
4 | 2240 | 8960
33 | 3072 | 101376

Another patch on top of that, switching rd_indexcxt to
GenerationContext (from AllocSet) sees the following improvement

count (patch 2) | total_bytes | combined_size
------------------+-------------+---------------
87 | | 118832
22 | 1680 | 36960
11 | 1968 | 21648
50 | 1024 | 51200
4 | 2256 | 9024

Also tracked: total memctx-tracked memory usage on a fresh connection [0]select count(*), total_bytes, sum(total_bytes) as "combined size" from pg_backend_memory_contexts WHERE name = 'index info' group by rollup (2);:

3ba2cdaa: 2006024 / 1959 kB
Master: 2063112 / 2015 kB
Patch 1: 2040648 / 1993 kB
Patch 2: 1976440 / 1930 kB

There isn't a lot of space on master to allocate new memory before it
reaches a (standard linux configuration) 128kB boundary - only 33kB
(assuming no other memory tracking overhead). It's easy to allocate
that much, and go over, causing malloc to extend with sbrk by 128kB.
If we then get back under because all per-query memory was released,
the newly allocated memory won't have any data anymore, and will get
released again immediately (default: release with sbrk when the top

=128kB is free), thus churning that memory area.

We may just have been lucky before, and your observation that
MALLOC_TOP_PAD_ >= 4MB fixes the issue reinforces that idea.

If patch 1 or patch 1+2 fixes this regression for you, then that's
another indication that we exceeded this threshold in a bad way.

Kind regards,

Matthias van de Meent
Neon (https://neon.tech)

PS. In ± 1 hour I'm leaving for pgconf.dev, so this will be my final
investigation update on the issue today CEST.

[0]: select count(*), total_bytes, sum(total_bytes) as "combined size" from pg_backend_memory_contexts WHERE name = 'index info' group by rollup (2);
from pg_backend_memory_contexts WHERE name = 'index info' group by
rollup (2);
[1]: select sum(total_bytes), pg_size_pretty(sum(total_bytes)) from pg_backend_memory_contexts;
pg_backend_memory_contexts;

Attachments:

v1-0002-Use-Generation-contexts-for-rd_indexcxt.patchapplication/octet-stream; name=v1-0002-Use-Generation-contexts-for-rd_indexcxt.patchDownload
From ad12cdbacda325b898b72313eaa08c2924d20e75 Mon Sep 17 00:00:00 2001
From: Matthias van de Meent <boekewurm+postgres@gmail.com>
Date: Sat, 10 May 2025 12:53:50 +0200
Subject: [PATCH v1 2/2] Use Generation contexts for rd_indexcxt

This removes completely the additional overhead caused by bucket sizing,
presumably making the system fast again.

On my local system, the memory usage is reduced by a further 52 kB
---
 src/backend/utils/cache/relcache.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index efeb65115ed..6ef857646d2 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1483,9 +1483,9 @@ RelationInitIndexAccessInfo(Relation relation)
 	 * a context, and not just a couple of pallocs, is so that we won't leak
 	 * any subsidiary info attached to fmgr lookup records.
 	 */
-	indexcxt = AllocSetContextCreate(CacheMemoryContext,
-									 "index info",
-									 ALLOCSET_SMALL_SIZES);
+	indexcxt = GenerationContextCreate(CacheMemoryContext,
+									   "index info",
+									   ALLOCSET_SMALL_SIZES);
 	relation->rd_indexcxt = indexcxt;
 	MemoryContextCopyAndSetIdentifier(indexcxt,
 									  RelationGetRelationName(relation));
@@ -6314,9 +6314,9 @@ load_relcache_init_file(bool shared)
 			 * prepare index info context --- parameters should match
 			 * RelationInitIndexAccessInfo
 			 */
-			indexcxt = AllocSetContextCreate(CacheMemoryContext,
-											 "index info",
-											 ALLOCSET_SMALL_SIZES);
+			indexcxt = GenerationContextCreate(CacheMemoryContext,
+											   "index info",
+											   ALLOCSET_SMALL_SIZES);
 			rel->rd_indexcxt = indexcxt;
 			MemoryContextCopyAndSetIdentifier(indexcxt,
 											  RelationGetRelationName(rel));
-- 
2.48.1

v1-0001-Stop-heap-allocating-IndexAmRoutine-for-every-ind.patchapplication/octet-stream; name=v1-0001-Stop-heap-allocating-IndexAmRoutine-for-every-ind.patchDownload
From 206d5c0da8acfa749744ecc8f12cd83bf940dd82 Mon Sep 17 00:00:00 2001
From: Matthias van de Meent <boekewurm+postgres@gmail.com>
Date: Sat, 10 May 2025 12:38:46 +0200
Subject: [PATCH v1 1/2] Stop heap-allocating IndexAmRoutine for every index

By making every IndexAmRoutine a static const*, we save a lot of memory.
---
 contrib/bloom/blutils.c                       | 111 ++++++++---------
 src/backend/access/brin/brin.c                | 113 +++++++++---------
 src/backend/access/gin/ginutil.c              | 109 ++++++++---------
 src/backend/access/gist/gist.c                | 113 +++++++++---------
 src/backend/access/hash/hash.c                | 113 +++++++++---------
 src/backend/access/index/amapi.c              |  12 +-
 src/backend/access/nbtree/nbtree.c            | 113 +++++++++---------
 src/backend/access/spgist/spgutils.c          | 113 +++++++++---------
 src/backend/catalog/index.c                   |   4 +-
 src/backend/commands/indexcmds.c              |   5 +-
 src/backend/commands/opclasscmds.c            |   8 +-
 src/backend/executor/execAmi.c                |   3 +-
 src/backend/optimizer/util/plancat.c          |   2 +-
 src/backend/utils/adt/amutils.c               |   4 +-
 src/backend/utils/adt/ruleutils.c             |   2 +-
 src/backend/utils/cache/lsyscache.c           |   9 +-
 src/backend/utils/cache/relcache.c            |  17 +--
 src/include/access/amapi.h                    |   4 +-
 src/include/utils/rel.h                       |   2 +-
 .../modules/dummy_index_am/dummy_index_am.c   | 101 ++++++++--------
 20 files changed, 472 insertions(+), 486 deletions(-)

diff --git a/contrib/bloom/blutils.c b/contrib/bloom/blutils.c
index 2c0e71eedc6..7b45993e096 100644
--- a/contrib/bloom/blutils.c
+++ b/contrib/bloom/blutils.c
@@ -102,61 +102,62 @@ makeDefaultBloomOptions(void)
 Datum
 blhandler(PG_FUNCTION_ARGS)
 {
-	IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
-
-	amroutine->amstrategies = BLOOM_NSTRATEGIES;
-	amroutine->amsupport = BLOOM_NPROC;
-	amroutine->amoptsprocnum = BLOOM_OPTIONS_PROC;
-	amroutine->amcanorder = false;
-	amroutine->amcanorderbyop = false;
-	amroutine->amcanhash = false;
-	amroutine->amconsistentequality = false;
-	amroutine->amconsistentordering = false;
-	amroutine->amcanbackward = false;
-	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = true;
-	amroutine->amoptionalkey = true;
-	amroutine->amsearcharray = false;
-	amroutine->amsearchnulls = false;
-	amroutine->amstorage = false;
-	amroutine->amclusterable = false;
-	amroutine->ampredlocks = false;
-	amroutine->amcanparallel = false;
-	amroutine->amcanbuildparallel = false;
-	amroutine->amcaninclude = false;
-	amroutine->amusemaintenanceworkmem = false;
-	amroutine->amparallelvacuumoptions =
-		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_CLEANUP;
-	amroutine->amkeytype = InvalidOid;
-
-	amroutine->ambuild = blbuild;
-	amroutine->ambuildempty = blbuildempty;
-	amroutine->aminsert = blinsert;
-	amroutine->aminsertcleanup = NULL;
-	amroutine->ambulkdelete = blbulkdelete;
-	amroutine->amvacuumcleanup = blvacuumcleanup;
-	amroutine->amcanreturn = NULL;
-	amroutine->amcostestimate = blcostestimate;
-	amroutine->amgettreeheight = NULL;
-	amroutine->amoptions = bloptions;
-	amroutine->amproperty = NULL;
-	amroutine->ambuildphasename = NULL;
-	amroutine->amvalidate = blvalidate;
-	amroutine->amadjustmembers = NULL;
-	amroutine->ambeginscan = blbeginscan;
-	amroutine->amrescan = blrescan;
-	amroutine->amgettuple = NULL;
-	amroutine->amgetbitmap = blgetbitmap;
-	amroutine->amendscan = blendscan;
-	amroutine->ammarkpos = NULL;
-	amroutine->amrestrpos = NULL;
-	amroutine->amestimateparallelscan = NULL;
-	amroutine->aminitparallelscan = NULL;
-	amroutine->amparallelrescan = NULL;
-	amroutine->amtranslatestrategy = NULL;
-	amroutine->amtranslatecmptype = NULL;
-
-	PG_RETURN_POINTER(amroutine);
+	static const IndexAmRoutine amroutine = {
+		.type = T_IndexAmRoutine,
+		.amstrategies = BLOOM_NSTRATEGIES,
+		.amsupport = BLOOM_NPROC,
+		.amoptsprocnum = BLOOM_OPTIONS_PROC,
+		.amcanorder = false,
+		.amcanorderbyop = false,
+		.amcanhash = false,
+		.amconsistentequality = false,
+		.amconsistentordering = false,
+		.amcanbackward = false,
+		.amcanunique = false,
+		.amcanmulticol = true,
+		.amoptionalkey = true,
+		.amsearcharray = false,
+		.amsearchnulls = false,
+		.amstorage = false,
+		.amclusterable = false,
+		.ampredlocks = false,
+		.amcanparallel = false,
+		.amcanbuildparallel = false,
+		.amcaninclude = false,
+		.amusemaintenanceworkmem = false,
+		.amparallelvacuumoptions =
+		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_CLEANUP,
+		.amkeytype = InvalidOid,
+
+		.ambuild = blbuild,
+		.ambuildempty = blbuildempty,
+		.aminsert = blinsert,
+		.aminsertcleanup = NULL,
+		.ambulkdelete = blbulkdelete,
+		.amvacuumcleanup = blvacuumcleanup,
+		.amcanreturn = NULL,
+		.amcostestimate = blcostestimate,
+		.amgettreeheight = NULL,
+		.amoptions = bloptions,
+		.amproperty = NULL,
+		.ambuildphasename = NULL,
+		.amvalidate = blvalidate,
+		.amadjustmembers = NULL,
+		.ambeginscan = blbeginscan,
+		.amrescan = blrescan,
+		.amgettuple = NULL,
+		.amgetbitmap = blgetbitmap,
+		.amendscan = blendscan,
+		.ammarkpos = NULL,
+		.amrestrpos = NULL,
+		.amestimateparallelscan = NULL,
+		.aminitparallelscan = NULL,
+		.amparallelrescan = NULL,
+		.amtranslatestrategy = NULL,
+		.amtranslatecmptype = NULL,
+	};
+
+	PG_RETURN_POINTER(&amroutine);
 }
 
 /*
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 01e1db7f856..22a24db49a7 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -249,62 +249,63 @@ static void _brin_parallel_scan_and_build(BrinBuildState *state,
 Datum
 brinhandler(PG_FUNCTION_ARGS)
 {
-	IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
-
-	amroutine->amstrategies = 0;
-	amroutine->amsupport = BRIN_LAST_OPTIONAL_PROCNUM;
-	amroutine->amoptsprocnum = BRIN_PROCNUM_OPTIONS;
-	amroutine->amcanorder = false;
-	amroutine->amcanorderbyop = false;
-	amroutine->amcanhash = false;
-	amroutine->amconsistentequality = false;
-	amroutine->amconsistentordering = false;
-	amroutine->amcanbackward = false;
-	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = true;
-	amroutine->amoptionalkey = true;
-	amroutine->amsearcharray = false;
-	amroutine->amsearchnulls = true;
-	amroutine->amstorage = true;
-	amroutine->amclusterable = false;
-	amroutine->ampredlocks = false;
-	amroutine->amcanparallel = false;
-	amroutine->amcanbuildparallel = true;
-	amroutine->amcaninclude = false;
-	amroutine->amusemaintenanceworkmem = false;
-	amroutine->amsummarizing = true;
-	amroutine->amparallelvacuumoptions =
-		VACUUM_OPTION_PARALLEL_CLEANUP;
-	amroutine->amkeytype = InvalidOid;
-
-	amroutine->ambuild = brinbuild;
-	amroutine->ambuildempty = brinbuildempty;
-	amroutine->aminsert = brininsert;
-	amroutine->aminsertcleanup = brininsertcleanup;
-	amroutine->ambulkdelete = brinbulkdelete;
-	amroutine->amvacuumcleanup = brinvacuumcleanup;
-	amroutine->amcanreturn = NULL;
-	amroutine->amcostestimate = brincostestimate;
-	amroutine->amgettreeheight = NULL;
-	amroutine->amoptions = brinoptions;
-	amroutine->amproperty = NULL;
-	amroutine->ambuildphasename = NULL;
-	amroutine->amvalidate = brinvalidate;
-	amroutine->amadjustmembers = NULL;
-	amroutine->ambeginscan = brinbeginscan;
-	amroutine->amrescan = brinrescan;
-	amroutine->amgettuple = NULL;
-	amroutine->amgetbitmap = bringetbitmap;
-	amroutine->amendscan = brinendscan;
-	amroutine->ammarkpos = NULL;
-	amroutine->amrestrpos = NULL;
-	amroutine->amestimateparallelscan = NULL;
-	amroutine->aminitparallelscan = NULL;
-	amroutine->amparallelrescan = NULL;
-	amroutine->amtranslatestrategy = NULL;
-	amroutine->amtranslatecmptype = NULL;
-
-	PG_RETURN_POINTER(amroutine);
+	static const IndexAmRoutine amroutine = {
+		.type = T_IndexAmRoutine,
+		.amstrategies = 0,
+		.amsupport = BRIN_LAST_OPTIONAL_PROCNUM,
+		.amoptsprocnum = BRIN_PROCNUM_OPTIONS,
+		.amcanorder = false,
+		.amcanorderbyop = false,
+		.amcanhash = false,
+		.amconsistentequality = false,
+		.amconsistentordering = false,
+		.amcanbackward = false,
+		.amcanunique = false,
+		.amcanmulticol = true,
+		.amoptionalkey = true,
+		.amsearcharray = false,
+		.amsearchnulls = true,
+		.amstorage = true,
+		.amclusterable = false,
+		.ampredlocks = false,
+		.amcanparallel = false,
+		.amcanbuildparallel = true,
+		.amcaninclude = false,
+		.amusemaintenanceworkmem = false,
+		.amsummarizing = true,
+		.amparallelvacuumoptions =
+			VACUUM_OPTION_PARALLEL_CLEANUP,
+		.amkeytype = InvalidOid,
+
+		.ambuild = brinbuild,
+		.ambuildempty = brinbuildempty,
+		.aminsert = brininsert,
+		.aminsertcleanup = brininsertcleanup,
+		.ambulkdelete = brinbulkdelete,
+		.amvacuumcleanup = brinvacuumcleanup,
+		.amcanreturn = NULL,
+		.amcostestimate = brincostestimate,
+		.amgettreeheight = NULL,
+		.amoptions = brinoptions,
+		.amproperty = NULL,
+		.ambuildphasename = NULL,
+		.amvalidate = brinvalidate,
+		.amadjustmembers = NULL,
+		.ambeginscan = brinbeginscan,
+		.amrescan = brinrescan,
+		.amgettuple = NULL,
+		.amgetbitmap = bringetbitmap,
+		.amendscan = brinendscan,
+		.ammarkpos = NULL,
+		.amrestrpos = NULL,
+		.amestimateparallelscan = NULL,
+		.aminitparallelscan = NULL,
+		.amparallelrescan = NULL,
+		.amtranslatestrategy = NULL,
+		.amtranslatecmptype = NULL,
+	};
+
+	PG_RETURN_POINTER(&amroutine);
 }
 
 /*
diff --git a/src/backend/access/gin/ginutil.c b/src/backend/access/gin/ginutil.c
index 78f7b7a2495..5b086faf465 100644
--- a/src/backend/access/gin/ginutil.c
+++ b/src/backend/access/gin/ginutil.c
@@ -37,60 +37,61 @@
 Datum
 ginhandler(PG_FUNCTION_ARGS)
 {
-	IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
-
-	amroutine->amstrategies = 0;
-	amroutine->amsupport = GINNProcs;
-	amroutine->amoptsprocnum = GIN_OPTIONS_PROC;
-	amroutine->amcanorder = false;
-	amroutine->amcanorderbyop = false;
-	amroutine->amcanhash = false;
-	amroutine->amconsistentequality = false;
-	amroutine->amconsistentordering = false;
-	amroutine->amcanbackward = false;
-	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = true;
-	amroutine->amoptionalkey = true;
-	amroutine->amsearcharray = false;
-	amroutine->amsearchnulls = false;
-	amroutine->amstorage = true;
-	amroutine->amclusterable = false;
-	amroutine->ampredlocks = true;
-	amroutine->amcanparallel = false;
-	amroutine->amcanbuildparallel = true;
-	amroutine->amcaninclude = false;
-	amroutine->amusemaintenanceworkmem = true;
-	amroutine->amsummarizing = false;
-	amroutine->amparallelvacuumoptions =
-		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_CLEANUP;
-	amroutine->amkeytype = InvalidOid;
-
-	amroutine->ambuild = ginbuild;
-	amroutine->ambuildempty = ginbuildempty;
-	amroutine->aminsert = gininsert;
-	amroutine->aminsertcleanup = NULL;
-	amroutine->ambulkdelete = ginbulkdelete;
-	amroutine->amvacuumcleanup = ginvacuumcleanup;
-	amroutine->amcanreturn = NULL;
-	amroutine->amcostestimate = gincostestimate;
-	amroutine->amgettreeheight = NULL;
-	amroutine->amoptions = ginoptions;
-	amroutine->amproperty = NULL;
-	amroutine->ambuildphasename = ginbuildphasename;
-	amroutine->amvalidate = ginvalidate;
-	amroutine->amadjustmembers = ginadjustmembers;
-	amroutine->ambeginscan = ginbeginscan;
-	amroutine->amrescan = ginrescan;
-	amroutine->amgettuple = NULL;
-	amroutine->amgetbitmap = gingetbitmap;
-	amroutine->amendscan = ginendscan;
-	amroutine->ammarkpos = NULL;
-	amroutine->amrestrpos = NULL;
-	amroutine->amestimateparallelscan = NULL;
-	amroutine->aminitparallelscan = NULL;
-	amroutine->amparallelrescan = NULL;
-
-	PG_RETURN_POINTER(amroutine);
+	static const IndexAmRoutine amroutine = {
+		.type = T_IndexAmRoutine,
+		.amstrategies = 0,
+		.amsupport = GINNProcs,
+		.amoptsprocnum = GIN_OPTIONS_PROC,
+		.amcanorder = false,
+		.amcanorderbyop = false,
+		.amcanhash = false,
+		.amconsistentequality = false,
+		.amconsistentordering = false,
+		.amcanbackward = false,
+		.amcanunique = false,
+		.amcanmulticol = true,
+		.amoptionalkey = true,
+		.amsearcharray = false,
+		.amsearchnulls = false,
+		.amstorage = true,
+		.amclusterable = false,
+		.ampredlocks = true,
+		.amcanparallel = false,
+		.amcanbuildparallel = true,
+		.amcaninclude = false,
+		.amusemaintenanceworkmem = true,
+		.amsummarizing = false,
+		.amparallelvacuumoptions =
+			VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_CLEANUP,
+		.amkeytype = InvalidOid,
+
+		.ambuild = ginbuild,
+		.ambuildempty = ginbuildempty,
+		.aminsert = gininsert,
+		.aminsertcleanup = NULL,
+		.ambulkdelete = ginbulkdelete,
+		.amvacuumcleanup = ginvacuumcleanup,
+		.amcanreturn = NULL,
+		.amcostestimate = gincostestimate,
+		.amgettreeheight = NULL,
+		.amoptions = ginoptions,
+		.amproperty = NULL,
+		.ambuildphasename = ginbuildphasename,
+		.amvalidate = ginvalidate,
+		.amadjustmembers = ginadjustmembers,
+		.ambeginscan = ginbeginscan,
+		.amrescan = ginrescan,
+		.amgettuple = NULL,
+		.amgetbitmap = gingetbitmap,
+		.amendscan = ginendscan,
+		.ammarkpos = NULL,
+		.amrestrpos = NULL,
+		.amestimateparallelscan = NULL,
+		.aminitparallelscan = NULL,
+		.amparallelrescan = NULL,
+	};
+
+	PG_RETURN_POINTER(&amroutine);
 }
 
 /*
diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c
index 7b24380c978..3641b41f7cf 100644
--- a/src/backend/access/gist/gist.c
+++ b/src/backend/access/gist/gist.c
@@ -58,62 +58,63 @@ static void gistprunepage(Relation rel, Page page, Buffer buffer,
 Datum
 gisthandler(PG_FUNCTION_ARGS)
 {
-	IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
-
-	amroutine->amstrategies = 0;
-	amroutine->amsupport = GISTNProcs;
-	amroutine->amoptsprocnum = GIST_OPTIONS_PROC;
-	amroutine->amcanorder = false;
-	amroutine->amcanorderbyop = true;
-	amroutine->amcanhash = false;
-	amroutine->amconsistentequality = false;
-	amroutine->amconsistentordering = false;
-	amroutine->amcanbackward = false;
-	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = true;
-	amroutine->amoptionalkey = true;
-	amroutine->amsearcharray = false;
-	amroutine->amsearchnulls = true;
-	amroutine->amstorage = true;
-	amroutine->amclusterable = true;
-	amroutine->ampredlocks = true;
-	amroutine->amcanparallel = false;
-	amroutine->amcanbuildparallel = false;
-	amroutine->amcaninclude = true;
-	amroutine->amusemaintenanceworkmem = false;
-	amroutine->amsummarizing = false;
-	amroutine->amparallelvacuumoptions =
-		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
-	amroutine->amkeytype = InvalidOid;
-
-	amroutine->ambuild = gistbuild;
-	amroutine->ambuildempty = gistbuildempty;
-	amroutine->aminsert = gistinsert;
-	amroutine->aminsertcleanup = NULL;
-	amroutine->ambulkdelete = gistbulkdelete;
-	amroutine->amvacuumcleanup = gistvacuumcleanup;
-	amroutine->amcanreturn = gistcanreturn;
-	amroutine->amcostestimate = gistcostestimate;
-	amroutine->amgettreeheight = NULL;
-	amroutine->amoptions = gistoptions;
-	amroutine->amproperty = gistproperty;
-	amroutine->ambuildphasename = NULL;
-	amroutine->amvalidate = gistvalidate;
-	amroutine->amadjustmembers = gistadjustmembers;
-	amroutine->ambeginscan = gistbeginscan;
-	amroutine->amrescan = gistrescan;
-	amroutine->amgettuple = gistgettuple;
-	amroutine->amgetbitmap = gistgetbitmap;
-	amroutine->amendscan = gistendscan;
-	amroutine->ammarkpos = NULL;
-	amroutine->amrestrpos = NULL;
-	amroutine->amestimateparallelscan = NULL;
-	amroutine->aminitparallelscan = NULL;
-	amroutine->amparallelrescan = NULL;
-	amroutine->amtranslatestrategy = NULL;
-	amroutine->amtranslatecmptype = gisttranslatecmptype;
-
-	PG_RETURN_POINTER(amroutine);
+	static const IndexAmRoutine amroutine = {
+		.type = T_IndexAmRoutine,
+		.amstrategies = 0,
+		.amsupport = GISTNProcs,
+		.amoptsprocnum = GIST_OPTIONS_PROC,
+		.amcanorder = false,
+		.amcanorderbyop = true,
+		.amcanhash = false,
+		.amconsistentequality = false,
+		.amconsistentordering = false,
+		.amcanbackward = false,
+		.amcanunique = false,
+		.amcanmulticol = true,
+		.amoptionalkey = true,
+		.amsearcharray = false,
+		.amsearchnulls = true,
+		.amstorage = true,
+		.amclusterable = true,
+		.ampredlocks = true,
+		.amcanparallel = false,
+		.amcanbuildparallel = false,
+		.amcaninclude = true,
+		.amusemaintenanceworkmem = false,
+		.amsummarizing = false,
+		.amparallelvacuumoptions =
+			VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP,
+		.amkeytype = InvalidOid,
+
+		.ambuild = gistbuild,
+		.ambuildempty = gistbuildempty,
+		.aminsert = gistinsert,
+		.aminsertcleanup = NULL,
+		.ambulkdelete = gistbulkdelete,
+		.amvacuumcleanup = gistvacuumcleanup,
+		.amcanreturn = gistcanreturn,
+		.amcostestimate = gistcostestimate,
+		.amgettreeheight = NULL,
+		.amoptions = gistoptions,
+		.amproperty = gistproperty,
+		.ambuildphasename = NULL,
+		.amvalidate = gistvalidate,
+		.amadjustmembers = gistadjustmembers,
+		.ambeginscan = gistbeginscan,
+		.amrescan = gistrescan,
+		.amgettuple = gistgettuple,
+		.amgetbitmap = gistgetbitmap,
+		.amendscan = gistendscan,
+		.ammarkpos = NULL,
+		.amrestrpos = NULL,
+		.amestimateparallelscan = NULL,
+		.aminitparallelscan = NULL,
+		.amparallelrescan = NULL,
+		.amtranslatestrategy = NULL,
+		.amtranslatecmptype = gisttranslatecmptype,
+	};
+
+	PG_RETURN_POINTER(&amroutine);
 }
 
 /*
diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index 53061c819fb..353fc03b989 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -57,62 +57,63 @@ static void hashbuildCallback(Relation index,
 Datum
 hashhandler(PG_FUNCTION_ARGS)
 {
-	IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
-
-	amroutine->amstrategies = HTMaxStrategyNumber;
-	amroutine->amsupport = HASHNProcs;
-	amroutine->amoptsprocnum = HASHOPTIONS_PROC;
-	amroutine->amcanorder = false;
-	amroutine->amcanorderbyop = false;
-	amroutine->amcanhash = true;
-	amroutine->amconsistentequality = true;
-	amroutine->amconsistentordering = false;
-	amroutine->amcanbackward = true;
-	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = false;
-	amroutine->amoptionalkey = false;
-	amroutine->amsearcharray = false;
-	amroutine->amsearchnulls = false;
-	amroutine->amstorage = false;
-	amroutine->amclusterable = false;
-	amroutine->ampredlocks = true;
-	amroutine->amcanparallel = false;
-	amroutine->amcanbuildparallel = false;
-	amroutine->amcaninclude = false;
-	amroutine->amusemaintenanceworkmem = false;
-	amroutine->amsummarizing = false;
-	amroutine->amparallelvacuumoptions =
-		VACUUM_OPTION_PARALLEL_BULKDEL;
-	amroutine->amkeytype = INT4OID;
-
-	amroutine->ambuild = hashbuild;
-	amroutine->ambuildempty = hashbuildempty;
-	amroutine->aminsert = hashinsert;
-	amroutine->aminsertcleanup = NULL;
-	amroutine->ambulkdelete = hashbulkdelete;
-	amroutine->amvacuumcleanup = hashvacuumcleanup;
-	amroutine->amcanreturn = NULL;
-	amroutine->amcostestimate = hashcostestimate;
-	amroutine->amgettreeheight = NULL;
-	amroutine->amoptions = hashoptions;
-	amroutine->amproperty = NULL;
-	amroutine->ambuildphasename = NULL;
-	amroutine->amvalidate = hashvalidate;
-	amroutine->amadjustmembers = hashadjustmembers;
-	amroutine->ambeginscan = hashbeginscan;
-	amroutine->amrescan = hashrescan;
-	amroutine->amgettuple = hashgettuple;
-	amroutine->amgetbitmap = hashgetbitmap;
-	amroutine->amendscan = hashendscan;
-	amroutine->ammarkpos = NULL;
-	amroutine->amrestrpos = NULL;
-	amroutine->amestimateparallelscan = NULL;
-	amroutine->aminitparallelscan = NULL;
-	amroutine->amparallelrescan = NULL;
-	amroutine->amtranslatestrategy = hashtranslatestrategy;
-	amroutine->amtranslatecmptype = hashtranslatecmptype;
-
-	PG_RETURN_POINTER(amroutine);
+	static const IndexAmRoutine amroutine = {
+		.type = T_IndexAmRoutine,
+		.amstrategies = HTMaxStrategyNumber,
+		.amsupport = HASHNProcs,
+		.amoptsprocnum = HASHOPTIONS_PROC,
+		.amcanorder = false,
+		.amcanorderbyop = false,
+		.amcanhash = true,
+		.amconsistentequality = true,
+		.amconsistentordering = false,
+		.amcanbackward = true,
+		.amcanunique = false,
+		.amcanmulticol = false,
+		.amoptionalkey = false,
+		.amsearcharray = false,
+		.amsearchnulls = false,
+		.amstorage = false,
+		.amclusterable = false,
+		.ampredlocks = true,
+		.amcanparallel = false,
+		.amcanbuildparallel = false,
+		.amcaninclude = false,
+		.amusemaintenanceworkmem = false,
+		.amsummarizing = false,
+		.amparallelvacuumoptions =
+			VACUUM_OPTION_PARALLEL_BULKDEL,
+		.amkeytype = INT4OID,
+	
+		.ambuild = hashbuild,
+		.ambuildempty = hashbuildempty,
+		.aminsert = hashinsert,
+		.aminsertcleanup = NULL,
+		.ambulkdelete = hashbulkdelete,
+		.amvacuumcleanup = hashvacuumcleanup,
+		.amcanreturn = NULL,
+		.amcostestimate = hashcostestimate,
+		.amgettreeheight = NULL,
+		.amoptions = hashoptions,
+		.amproperty = NULL,
+		.ambuildphasename = NULL,
+		.amvalidate = hashvalidate,
+		.amadjustmembers = hashadjustmembers,
+		.ambeginscan = hashbeginscan,
+		.amrescan = hashrescan,
+		.amgettuple = hashgettuple,
+		.amgetbitmap = hashgetbitmap,
+		.amendscan = hashendscan,
+		.ammarkpos = NULL,
+		.amrestrpos = NULL,
+		.amestimateparallelscan = NULL,
+		.aminitparallelscan = NULL,
+		.amparallelrescan = NULL,
+		.amtranslatestrategy = hashtranslatestrategy,
+		.amtranslatecmptype = hashtranslatecmptype,
+	};
+
+	PG_RETURN_POINTER(&amroutine);
 }
 
 /*
diff --git a/src/backend/access/index/amapi.c b/src/backend/access/index/amapi.c
index f0f4f974bce..40e8c88924a 100644
--- a/src/backend/access/index/amapi.c
+++ b/src/backend/access/index/amapi.c
@@ -29,7 +29,7 @@
  * any catalog access.  It's therefore safe to use this while bootstrapping
  * indexes for the system catalogs.  relcache.c relies on that.
  */
-IndexAmRoutine *
+const IndexAmRoutine *
 GetIndexAmRoutine(Oid amhandler)
 {
 	Datum		datum;
@@ -52,7 +52,7 @@ GetIndexAmRoutine(Oid amhandler)
  * If the given OID isn't a valid index access method, returns NULL if
  * noerror is true, else throws error.
  */
-IndexAmRoutine *
+const IndexAmRoutine *
 GetIndexAmRoutineByAmId(Oid amoid, bool noerror)
 {
 	HeapTuple	tuple;
@@ -118,7 +118,7 @@ CompareType
 IndexAmTranslateStrategy(StrategyNumber strategy, Oid amoid, Oid opfamily, bool missing_ok)
 {
 	CompareType result;
-	IndexAmRoutine *amroutine;
+	const IndexAmRoutine *amroutine;
 
 	/* shortcut for common case */
 	if (amoid == BTREE_AM_OID &&
@@ -148,7 +148,7 @@ StrategyNumber
 IndexAmTranslateCompareType(CompareType cmptype, Oid amoid, Oid opfamily, bool missing_ok)
 {
 	StrategyNumber result;
-	IndexAmRoutine *amroutine;
+	const IndexAmRoutine *amroutine;
 
 	/* shortcut for common case */
 	if (amoid == BTREE_AM_OID &&
@@ -178,7 +178,7 @@ amvalidate(PG_FUNCTION_ARGS)
 	HeapTuple	classtup;
 	Form_pg_opclass classform;
 	Oid			amoid;
-	IndexAmRoutine *amroutine;
+	const IndexAmRoutine *amroutine;
 
 	classtup = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclassoid));
 	if (!HeapTupleIsValid(classtup))
@@ -197,7 +197,5 @@ amvalidate(PG_FUNCTION_ARGS)
 
 	result = amroutine->amvalidate(opclassoid);
 
-	pfree(amroutine);
-
 	PG_RETURN_BOOL(result);
 }
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 765659887af..f89067c90bd 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -114,62 +114,63 @@ static BTVacuumPosting btreevacuumposting(BTVacState *vstate,
 Datum
 bthandler(PG_FUNCTION_ARGS)
 {
-	IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
-
-	amroutine->amstrategies = BTMaxStrategyNumber;
-	amroutine->amsupport = BTNProcs;
-	amroutine->amoptsprocnum = BTOPTIONS_PROC;
-	amroutine->amcanorder = true;
-	amroutine->amcanorderbyop = false;
-	amroutine->amcanhash = false;
-	amroutine->amconsistentequality = true;
-	amroutine->amconsistentordering = true;
-	amroutine->amcanbackward = true;
-	amroutine->amcanunique = true;
-	amroutine->amcanmulticol = true;
-	amroutine->amoptionalkey = true;
-	amroutine->amsearcharray = true;
-	amroutine->amsearchnulls = true;
-	amroutine->amstorage = false;
-	amroutine->amclusterable = true;
-	amroutine->ampredlocks = true;
-	amroutine->amcanparallel = true;
-	amroutine->amcanbuildparallel = true;
-	amroutine->amcaninclude = true;
-	amroutine->amusemaintenanceworkmem = false;
-	amroutine->amsummarizing = false;
-	amroutine->amparallelvacuumoptions =
-		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
-	amroutine->amkeytype = InvalidOid;
-
-	amroutine->ambuild = btbuild;
-	amroutine->ambuildempty = btbuildempty;
-	amroutine->aminsert = btinsert;
-	amroutine->aminsertcleanup = NULL;
-	amroutine->ambulkdelete = btbulkdelete;
-	amroutine->amvacuumcleanup = btvacuumcleanup;
-	amroutine->amcanreturn = btcanreturn;
-	amroutine->amcostestimate = btcostestimate;
-	amroutine->amgettreeheight = btgettreeheight;
-	amroutine->amoptions = btoptions;
-	amroutine->amproperty = btproperty;
-	amroutine->ambuildphasename = btbuildphasename;
-	amroutine->amvalidate = btvalidate;
-	amroutine->amadjustmembers = btadjustmembers;
-	amroutine->ambeginscan = btbeginscan;
-	amroutine->amrescan = btrescan;
-	amroutine->amgettuple = btgettuple;
-	amroutine->amgetbitmap = btgetbitmap;
-	amroutine->amendscan = btendscan;
-	amroutine->ammarkpos = btmarkpos;
-	amroutine->amrestrpos = btrestrpos;
-	amroutine->amestimateparallelscan = btestimateparallelscan;
-	amroutine->aminitparallelscan = btinitparallelscan;
-	amroutine->amparallelrescan = btparallelrescan;
-	amroutine->amtranslatestrategy = bttranslatestrategy;
-	amroutine->amtranslatecmptype = bttranslatecmptype;
-
-	PG_RETURN_POINTER(amroutine);
+	static const IndexAmRoutine amroutine = {
+		.type = T_IndexAmRoutine,
+		.amstrategies = BTMaxStrategyNumber,
+		.amsupport = BTNProcs,
+		.amoptsprocnum = BTOPTIONS_PROC,
+		.amcanorder = true,
+		.amcanorderbyop = false,
+		.amcanhash = false,
+		.amconsistentequality = true,
+		.amconsistentordering = true,
+		.amcanbackward = true,
+		.amcanunique = true,
+		.amcanmulticol = true,
+		.amoptionalkey = true,
+		.amsearcharray = true,
+		.amsearchnulls = true,
+		.amstorage = false,
+		.amclusterable = true,
+		.ampredlocks = true,
+		.amcanparallel = true,
+		.amcanbuildparallel = true,
+		.amcaninclude = true,
+		.amusemaintenanceworkmem = false,
+		.amsummarizing = false,
+		.amparallelvacuumoptions =
+		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP,
+		.amkeytype = InvalidOid,
+
+		.ambuild = btbuild,
+		.ambuildempty = btbuildempty,
+		.aminsert = btinsert,
+		.aminsertcleanup = NULL,
+		.ambulkdelete = btbulkdelete,
+		.amvacuumcleanup = btvacuumcleanup,
+		.amcanreturn = btcanreturn,
+		.amcostestimate = btcostestimate,
+		.amgettreeheight = btgettreeheight,
+		.amoptions = btoptions,
+		.amproperty = btproperty,
+		.ambuildphasename = btbuildphasename,
+		.amvalidate = btvalidate,
+		.amadjustmembers = btadjustmembers,
+		.ambeginscan = btbeginscan,
+		.amrescan = btrescan,
+		.amgettuple = btgettuple,
+		.amgetbitmap = btgetbitmap,
+		.amendscan = btendscan,
+		.ammarkpos = btmarkpos,
+		.amrestrpos = btrestrpos,
+		.amestimateparallelscan = btestimateparallelscan,
+		.aminitparallelscan = btinitparallelscan,
+		.amparallelrescan = btparallelrescan,
+		.amtranslatestrategy = bttranslatestrategy,
+		.amtranslatecmptype = bttranslatecmptype,
+	};
+
+	PG_RETURN_POINTER(&amroutine);
 }
 
 /*
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 95fea74e296..935d7afbbde 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -43,62 +43,63 @@
 Datum
 spghandler(PG_FUNCTION_ARGS)
 {
-	IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
-
-	amroutine->amstrategies = 0;
-	amroutine->amsupport = SPGISTNProc;
-	amroutine->amoptsprocnum = SPGIST_OPTIONS_PROC;
-	amroutine->amcanorder = false;
-	amroutine->amcanorderbyop = true;
-	amroutine->amcanhash = false;
-	amroutine->amconsistentequality = false;
-	amroutine->amconsistentordering = false;
-	amroutine->amcanbackward = false;
-	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = false;
-	amroutine->amoptionalkey = true;
-	amroutine->amsearcharray = false;
-	amroutine->amsearchnulls = true;
-	amroutine->amstorage = true;
-	amroutine->amclusterable = false;
-	amroutine->ampredlocks = false;
-	amroutine->amcanparallel = false;
-	amroutine->amcanbuildparallel = false;
-	amroutine->amcaninclude = true;
-	amroutine->amusemaintenanceworkmem = false;
-	amroutine->amsummarizing = false;
-	amroutine->amparallelvacuumoptions =
-		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
-	amroutine->amkeytype = InvalidOid;
-
-	amroutine->ambuild = spgbuild;
-	amroutine->ambuildempty = spgbuildempty;
-	amroutine->aminsert = spginsert;
-	amroutine->aminsertcleanup = NULL;
-	amroutine->ambulkdelete = spgbulkdelete;
-	amroutine->amvacuumcleanup = spgvacuumcleanup;
-	amroutine->amcanreturn = spgcanreturn;
-	amroutine->amcostestimate = spgcostestimate;
-	amroutine->amgettreeheight = NULL;
-	amroutine->amoptions = spgoptions;
-	amroutine->amproperty = spgproperty;
-	amroutine->ambuildphasename = NULL;
-	amroutine->amvalidate = spgvalidate;
-	amroutine->amadjustmembers = spgadjustmembers;
-	amroutine->ambeginscan = spgbeginscan;
-	amroutine->amrescan = spgrescan;
-	amroutine->amgettuple = spggettuple;
-	amroutine->amgetbitmap = spggetbitmap;
-	amroutine->amendscan = spgendscan;
-	amroutine->ammarkpos = NULL;
-	amroutine->amrestrpos = NULL;
-	amroutine->amestimateparallelscan = NULL;
-	amroutine->aminitparallelscan = NULL;
-	amroutine->amparallelrescan = NULL;
-	amroutine->amtranslatestrategy = NULL;
-	amroutine->amtranslatecmptype = NULL;
-
-	PG_RETURN_POINTER(amroutine);
+	static const IndexAmRoutine amroutine = {
+		.type = T_IndexAmRoutine,
+		.amstrategies = 0,
+		.amsupport = SPGISTNProc,
+		.amoptsprocnum = SPGIST_OPTIONS_PROC,
+		.amcanorder = false,
+		.amcanorderbyop = true,
+		.amcanhash = false,
+		.amconsistentequality = false,
+		.amconsistentordering = false,
+		.amcanbackward = false,
+		.amcanunique = false,
+		.amcanmulticol = false,
+		.amoptionalkey = true,
+		.amsearcharray = false,
+		.amsearchnulls = true,
+		.amstorage = true,
+		.amclusterable = false,
+		.ampredlocks = false,
+		.amcanparallel = false,
+		.amcanbuildparallel = false,
+		.amcaninclude = true,
+		.amusemaintenanceworkmem = false,
+		.amsummarizing = false,
+		.amparallelvacuumoptions =
+		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP,
+		.amkeytype = InvalidOid,
+
+		.ambuild = spgbuild,
+		.ambuildempty = spgbuildempty,
+		.aminsert = spginsert,
+		.aminsertcleanup = NULL,
+		.ambulkdelete = spgbulkdelete,
+		.amvacuumcleanup = spgvacuumcleanup,
+		.amcanreturn = spgcanreturn,
+		.amcostestimate = spgcostestimate,
+		.amgettreeheight = NULL,
+		.amoptions = spgoptions,
+		.amproperty = spgproperty,
+		.ambuildphasename = NULL,
+		.amvalidate = spgvalidate,
+		.amadjustmembers = spgadjustmembers,
+		.ambeginscan = spgbeginscan,
+		.amrescan = spgrescan,
+		.amgettuple = spggettuple,
+		.amgetbitmap = spggetbitmap,
+		.amendscan = spgendscan,
+		.ammarkpos = NULL,
+		.amrestrpos = NULL,
+		.amestimateparallelscan = NULL,
+		.aminitparallelscan = NULL,
+		.amparallelrescan = NULL,
+		.amtranslatestrategy = NULL,
+		.amtranslatecmptype = NULL,
+	};
+
+	PG_RETURN_POINTER(&amroutine);
 }
 
 /*
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 739a92bdcc1..8253d9a8ddb 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -289,7 +289,7 @@ ConstructTupleDescriptor(Relation heapRelation,
 	int			numkeyatts = indexInfo->ii_NumIndexKeyAttrs;
 	ListCell   *colnames_item = list_head(indexColNames);
 	ListCell   *indexpr_item = list_head(indexInfo->ii_Expressions);
-	IndexAmRoutine *amroutine;
+	const IndexAmRoutine *amroutine;
 	TupleDesc	heapTupDesc;
 	TupleDesc	indexTupDesc;
 	int			natts;			/* #atts in heap rel --- for error checks */
@@ -481,8 +481,6 @@ ConstructTupleDescriptor(Relation heapRelation,
 		populate_compact_attribute(indexTupDesc, i);
 	}
 
-	pfree(amroutine);
-
 	return indexTupDesc;
 }
 
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 33c2106c17c..e4caeac0147 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -191,7 +191,7 @@ CheckIndexCompatible(Oid oldId,
 	HeapTuple	tuple;
 	Form_pg_index indexForm;
 	Form_pg_am	accessMethodForm;
-	IndexAmRoutine *amRoutine;
+	const IndexAmRoutine *amRoutine;
 	bool		amcanorder;
 	bool		amsummarizing;
 	int16	   *coloptions;
@@ -567,7 +567,7 @@ DefineIndex(Oid tableId,
 	Relation	rel;
 	HeapTuple	tuple;
 	Form_pg_am	accessMethodForm;
-	IndexAmRoutine *amRoutine;
+	const IndexAmRoutine *amRoutine;
 	bool		amcanorder;
 	bool		amissummarizing;
 	amoptions_function amoptions;
@@ -896,7 +896,6 @@ DefineIndex(Oid tableId,
 	amoptions = amRoutine->amoptions;
 	amissummarizing = amRoutine->amsummarizing;
 
-	pfree(amRoutine);
 	ReleaseSysCache(tuple);
 
 	/*
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index a6dd8eab518..7c85eb5fcc1 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -349,7 +349,7 @@ DefineOpClass(CreateOpClassStmt *stmt)
 	Relation	rel;
 	HeapTuple	tup;
 	Form_pg_am	amform;
-	IndexAmRoutine *amroutine;
+	const IndexAmRoutine *amroutine;
 	Datum		values[Natts_pg_opclass];
 	bool		nulls[Natts_pg_opclass];
 	AclResult	aclresult;
@@ -823,7 +823,7 @@ AlterOpFamily(AlterOpFamilyStmt *stmt)
 				maxProcNumber;	/* amsupport value */
 	HeapTuple	tup;
 	Form_pg_am	amform;
-	IndexAmRoutine *amroutine;
+	const IndexAmRoutine *amroutine;
 
 	/* Get necessary info about access method */
 	tup = SearchSysCache1(AMNAME, CStringGetDatum(stmt->amname));
@@ -882,7 +882,7 @@ AlterOpFamilyAdd(AlterOpFamilyStmt *stmt, Oid amoid, Oid opfamilyoid,
 				 int maxOpNumber, int maxProcNumber, int optsProcNumber,
 				 List *items)
 {
-	IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(amoid, false);
+	const IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(amoid, false);
 	List	   *operators;		/* OpFamilyMember list for operators */
 	List	   *procedures;		/* OpFamilyMember list for support procs */
 	ListCell   *l;
@@ -1165,7 +1165,7 @@ assignOperTypes(OpFamilyMember *member, Oid amoid, Oid typeoid)
 		 * the family has been created but not yet populated with the required
 		 * operators.)
 		 */
-		IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(amoid, false);
+		const IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(amoid, false);
 
 		if (!amroutine->amcanorderbyop)
 			ereport(ERROR,
diff --git a/src/backend/executor/execAmi.c b/src/backend/executor/execAmi.c
index 1d0e8ad57b4..4893ea3ce91 100644
--- a/src/backend/executor/execAmi.c
+++ b/src/backend/executor/execAmi.c
@@ -605,7 +605,7 @@ IndexSupportsBackwardScan(Oid indexid)
 	bool		result;
 	HeapTuple	ht_idxrel;
 	Form_pg_class idxrelrec;
-	IndexAmRoutine *amroutine;
+	const IndexAmRoutine *amroutine;
 
 	/* Fetch the pg_class tuple of the index relation */
 	ht_idxrel = SearchSysCache1(RELOID, ObjectIdGetDatum(indexid));
@@ -618,7 +618,6 @@ IndexSupportsBackwardScan(Oid indexid)
 
 	result = amroutine->amcanbackward;
 
-	pfree(amroutine);
 	ReleaseSysCache(ht_idxrel);
 
 	return result;
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 59233b64730..8180a5e418c 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -243,7 +243,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 			Oid			indexoid = lfirst_oid(l);
 			Relation	indexRelation;
 			Form_pg_index index;
-			IndexAmRoutine *amroutine = NULL;
+			const IndexAmRoutine *amroutine = NULL;
 			IndexOptInfo *info;
 			int			ncolumns,
 						nkeycolumns;
diff --git a/src/backend/utils/adt/amutils.c b/src/backend/utils/adt/amutils.c
index 0af26d6acfa..59c0112c696 100644
--- a/src/backend/utils/adt/amutils.c
+++ b/src/backend/utils/adt/amutils.c
@@ -156,7 +156,7 @@ indexam_property(FunctionCallInfo fcinfo,
 	bool		isnull = false;
 	int			natts = 0;
 	IndexAMProperty prop;
-	IndexAmRoutine *routine;
+	const IndexAmRoutine *routine;
 
 	/* Try to convert property name to enum (no error if not known) */
 	prop = lookup_prop_name(propname);
@@ -452,7 +452,7 @@ pg_indexam_progress_phasename(PG_FUNCTION_ARGS)
 {
 	Oid			amoid = PG_GETARG_OID(0);
 	int32		phasenum = PG_GETARG_INT32(1);
-	IndexAmRoutine *routine;
+	const IndexAmRoutine *routine;
 	char	   *name;
 
 	routine = GetIndexAmRoutineByAmId(amoid, true);
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 467b08198b8..a9a2184e90b 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -1281,7 +1281,7 @@ pg_get_indexdef_worker(Oid indexrelid, int colno,
 	Form_pg_index idxrec;
 	Form_pg_class idxrelrec;
 	Form_pg_am	amrec;
-	IndexAmRoutine *amroutine;
+	const IndexAmRoutine *amroutine;
 	List	   *indexprs;
 	ListCell   *indexpr_item;
 	List	   *context;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index c460a72b75d..c927bef1224 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -232,10 +232,9 @@ get_opmethod_canorder(Oid amoid)
 		default:
 			{
 				bool		result;
-				IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(amoid, false);
+				const IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(amoid, false);
 
 				result = amroutine->amcanorder;
-				pfree(amroutine);
 				return result;
 			}
 	}
@@ -729,7 +728,7 @@ get_op_index_interpretation(Oid opno)
 			{
 				HeapTuple	op_tuple = &catlist->members[i]->tuple;
 				Form_pg_amop op_form = (Form_pg_amop) GETSTRUCT(op_tuple);
-				IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(op_form->amopmethod, false);
+				const IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(op_form->amopmethod, false);
 				CompareType cmptype;
 
 				/* must be ordering index */
@@ -803,7 +802,7 @@ equality_ops_are_compatible(Oid opno1, Oid opno2)
 		 */
 		if (op_in_opfamily(opno2, op_form->amopfamily))
 		{
-			IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(op_form->amopmethod, false);
+			const IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(op_form->amopmethod, false);
 
 			if (amroutine->amconsistentequality)
 			{
@@ -859,7 +858,7 @@ comparison_ops_are_compatible(Oid opno1, Oid opno2)
 		 */
 		if (op_in_opfamily(opno2, op_form->amopfamily))
 		{
-			IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(op_form->amopmethod, false);
+			const IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(op_form->amopmethod, false);
 
 			if (amroutine->amconsistentordering)
 			{
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 68ff67de549..efeb65115ed 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1420,22 +1420,7 @@ RelationInitPhysicalAddr(Relation relation)
 static void
 InitIndexAmRoutine(Relation relation)
 {
-	IndexAmRoutine *cached,
-			   *tmp;
-
-	/*
-	 * Call the amhandler in current, short-lived memory context, just in case
-	 * it leaks anything (it probably won't, but let's be paranoid).
-	 */
-	tmp = GetIndexAmRoutine(relation->rd_amhandler);
-
-	/* OK, now transfer the data into relation's rd_indexcxt. */
-	cached = (IndexAmRoutine *) MemoryContextAlloc(relation->rd_indexcxt,
-												   sizeof(IndexAmRoutine));
-	memcpy(cached, tmp, sizeof(IndexAmRoutine));
-	relation->rd_indam = cached;
-
-	pfree(tmp);
+	relation->rd_indam = GetIndexAmRoutine(relation->rd_amhandler);
 }
 
 /*
diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index 52916bab7a3..de934396fa2 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -324,8 +324,8 @@ typedef struct IndexAmRoutine
 
 
 /* Functions in access/index/amapi.c */
-extern IndexAmRoutine *GetIndexAmRoutine(Oid amhandler);
-extern IndexAmRoutine *GetIndexAmRoutineByAmId(Oid amoid, bool noerror);
+extern const IndexAmRoutine *GetIndexAmRoutine(Oid amhandler);
+extern const IndexAmRoutine *GetIndexAmRoutineByAmId(Oid amoid, bool noerror);
 extern CompareType IndexAmTranslateStrategy(StrategyNumber strategy, Oid amoid, Oid opfamily, bool missing_ok);
 extern StrategyNumber IndexAmTranslateCompareType(CompareType cmptype, Oid amoid, Oid opfamily, bool missing_ok);
 
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index b552359915f..87b776c98fe 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -203,7 +203,7 @@ typedef struct RelationData
 	 */
 	MemoryContext rd_indexcxt;	/* private memory cxt for this stuff */
 	/* use "struct" here to avoid needing to include amapi.h: */
-	struct IndexAmRoutine *rd_indam;	/* index AM's API struct */
+	const struct IndexAmRoutine *rd_indam;	/* index AM's API struct */
 	Oid		   *rd_opfamily;	/* OIDs of op families for each index col */
 	Oid		   *rd_opcintype;	/* OIDs of opclass declared input data types */
 	RegProcedure *rd_support;	/* OIDs of support procedures */
diff --git a/src/test/modules/dummy_index_am/dummy_index_am.c b/src/test/modules/dummy_index_am/dummy_index_am.c
index 94ef639b6fc..98a603892f4 100644
--- a/src/test/modules/dummy_index_am/dummy_index_am.c
+++ b/src/test/modules/dummy_index_am/dummy_index_am.c
@@ -276,56 +276,57 @@ diendscan(IndexScanDesc scan)
 Datum
 dihandler(PG_FUNCTION_ARGS)
 {
-	IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
-
-	amroutine->amstrategies = 0;
-	amroutine->amsupport = 1;
-	amroutine->amcanorder = false;
-	amroutine->amcanorderbyop = false;
-	amroutine->amcanhash = false;
-	amroutine->amconsistentequality = false;
-	amroutine->amconsistentordering = false;
-	amroutine->amcanbackward = false;
-	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = false;
-	amroutine->amoptionalkey = false;
-	amroutine->amsearcharray = false;
-	amroutine->amsearchnulls = false;
-	amroutine->amstorage = false;
-	amroutine->amclusterable = false;
-	amroutine->ampredlocks = false;
-	amroutine->amcanparallel = false;
-	amroutine->amcanbuildparallel = false;
-	amroutine->amcaninclude = false;
-	amroutine->amusemaintenanceworkmem = false;
-	amroutine->amsummarizing = false;
-	amroutine->amparallelvacuumoptions = VACUUM_OPTION_NO_PARALLEL;
-	amroutine->amkeytype = InvalidOid;
-
-	amroutine->ambuild = dibuild;
-	amroutine->ambuildempty = dibuildempty;
-	amroutine->aminsert = diinsert;
-	amroutine->ambulkdelete = dibulkdelete;
-	amroutine->amvacuumcleanup = divacuumcleanup;
-	amroutine->amcanreturn = NULL;
-	amroutine->amcostestimate = dicostestimate;
-	amroutine->amgettreeheight = NULL;
-	amroutine->amoptions = dioptions;
-	amroutine->amproperty = NULL;
-	amroutine->ambuildphasename = NULL;
-	amroutine->amvalidate = divalidate;
-	amroutine->ambeginscan = dibeginscan;
-	amroutine->amrescan = direscan;
-	amroutine->amgettuple = NULL;
-	amroutine->amgetbitmap = NULL;
-	amroutine->amendscan = diendscan;
-	amroutine->ammarkpos = NULL;
-	amroutine->amrestrpos = NULL;
-	amroutine->amestimateparallelscan = NULL;
-	amroutine->aminitparallelscan = NULL;
-	amroutine->amparallelrescan = NULL;
-
-	PG_RETURN_POINTER(amroutine);
+	static const IndexAmRoutine amroutine = {
+		.type = T_IndexAmRoutine,
+		.amstrategies = 0,
+		.amsupport = 1,
+		.amcanorder = false,
+		.amcanorderbyop = false,
+		.amcanhash = false,
+		.amconsistentequality = false,
+		.amconsistentordering = false,
+		.amcanbackward = false,
+		.amcanunique = false,
+		.amcanmulticol = false,
+		.amoptionalkey = false,
+		.amsearcharray = false,
+		.amsearchnulls = false,
+		.amstorage = false,
+		.amclusterable = false,
+		.ampredlocks = false,
+		.amcanparallel = false,
+		.amcanbuildparallel = false,
+		.amcaninclude = false,
+		.amusemaintenanceworkmem = false,
+		.amsummarizing = false,
+		.amparallelvacuumoptions = VACUUM_OPTION_NO_PARALLEL,
+		.amkeytype = InvalidOid,
+	
+		.ambuild = dibuild,
+		.ambuildempty = dibuildempty,
+		.aminsert = diinsert,
+		.ambulkdelete = dibulkdelete,
+		.amvacuumcleanup = divacuumcleanup,
+		.amcanreturn = NULL,
+		.amcostestimate = dicostestimate,
+		.amgettreeheight = NULL,
+		.amoptions = dioptions,
+		.amproperty = NULL,
+		.ambuildphasename = NULL,
+		.amvalidate = divalidate,
+		.ambeginscan = dibeginscan,
+		.amrescan = direscan,
+		.amgettuple = NULL,
+		.amgetbitmap = NULL,
+		.amendscan = diendscan,
+		.ammarkpos = NULL,
+		.amrestrpos = NULL,
+		.amestimateparallelscan = NULL,
+		.aminitparallelscan = NULL,
+		.amparallelrescan = NULL,
+	};
+
+	PG_RETURN_POINTER(&amroutine);
 }
 
 void
-- 
2.48.1

#132Tomas Vondra
tomas@vondra.me
In reply to: Matthias van de Meent (#131)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 5/10/25 13:14, Matthias van de Meent wrote:

...

I've attached a patch that makes IndexAmRoutine a static const*,
removing it from rd_indexcxt, and returning some of the index ctx
memory usage to normal:

count (patch 1) | total_bytes | combined_size
-----------------+-------------+---------------
87 | | 171776
10 | 2048 | 20480
40 | 1024 | 40960
4 | 2240 | 8960
33 | 3072 | 101376

Another patch on top of that, switching rd_indexcxt to
GenerationContext (from AllocSet) sees the following improvement

count (patch 2) | total_bytes | combined_size
------------------+-------------+---------------
87 | | 118832
22 | 1680 | 36960
11 | 1968 | 21648
50 | 1024 | 51200
4 | 2256 | 9024

Also tracked: total memctx-tracked memory usage on a fresh connection [0]:

3ba2cdaa: 2006024 / 1959 kB
Master: 2063112 / 2015 kB
Patch 1: 2040648 / 1993 kB
Patch 2: 1976440 / 1930 kB

There isn't a lot of space on master to allocate new memory before it
reaches a (standard linux configuration) 128kB boundary - only 33kB
(assuming no other memory tracking overhead). It's easy to allocate
that much, and go over, causing malloc to extend with sbrk by 128kB.
If we then get back under because all per-query memory was released,
the newly allocated memory won't have any data anymore, and will get
released again immediately (default: release with sbrk when the top

=128kB is free), thus churning that memory area.

We may just have been lucky before, and your observation that
MALLOC_TOP_PAD_ >= 4MB fixes the issue reinforces that idea.

If patch 1 or patch 1+2 fixes this regression for you, then that's
another indication that we exceeded this threshold in a bad way.

Thanks! I think this explanation seems very plausible. I repeated the
tests and the results agree with it too. Here's what I got for the two
older commits before/after skip scan, and then 0001 and 0001+0002:

old head 0001 0001+0002
mode clients 3ba2cdaa454 99ddf8615c2 54c23341b31 9a6f6679e67
----------------------------------------------------------------------
prepared 1 10858 3534 11109 3324
4 25311 11307 25325 10928
32 38869 14194 39423 13626
----------------------------------------------------------------------
simple 1 2676 1865 2534 1883
4 8355 6140 8012 6160
32 11827 7216 12046 7322

This is the bid=0 case, the bid=1 is very similar, I'm leaving it out to
keep this simple (and because formatting those tables is tedious). A
nicer table is in the attached PDF.

Relative to 3ba2cdaa454 it looks like this:

head 0001 0001+0002
mode clients 99ddf8615c2 54c23341b31 9a6f6679e67
--------------------------------------------------------
prepared 1 33% 102% 31%
4 45% 100% 43%
32 37% 101% 35%
--------------------------------------------------------
simple 1 70% 95% 70%
4 73% 96% 74%
32 61% 102% 62%

So clearly, 0001 helps a lot, essentially eliminating the regression.
But 0002 makes it slow again, so the generation context is not a good
match here (perhaps the rd_indexcxt allocation pattern is different).

Based on this I tried a couple additional experiments:

a) switch rd_indexcxt to ALLOCSET_DEFAULT_SIZES, speculating that maybe
one larger malloc() is cheaper than multiple smaller ones

b) increasing the ALLOC_CHUNK_FRACTION from 1/4 to 1/2, so that fewer
chunks need to be allocated as a separate block

c) switch rd_indexcxt to ALLOCSET_MEDIUM_SIZES, which is the same as
SMALL_SIZES, but INITSIZE is 2kB, combined with the CHUNK_FRACTION
adjustment from (b)

The results are in the second table in the PDF. None of it helped very
much, unfortunately. The (a) is even slower than master in some cases.
(b) helps in some cases, but not as much as 0001. And even 2kB blocks
make it slow again.

So I guess something like 0001 might be the way to go ...

But doesn't it also highlight how fragile this memory allocation is? The
skip scan patch didn't do anything wrong - it just added a couple
fields, using a little bit more memory. I think we understand allocating
more memory may need more time, but we expect the effect to be somewhat
proportional. Which doesn't seem to be the case here.

Many other patches add fields somewhere, it seems like bad luck the skip
scan happened to trigger this behavior. It's quite likely other patches
ran into the same issue, except that no one noticed. Maybe the skip scan
did that in much hotter code, not sure.

Of course, this is not "our" issue - it seems to be glibc specific
(based on my experience with allocators in other libc libraries). Still,
it's a long-standing behavior, and I doubt it's likely to change. But
considering glibc is what most systems use, maybe we should add some
protections?

I recall there were proposals to add optional mallopt() call to set the
M_TOP_PAD when running on glibc. Maybe we should revive that. I also had
a patch to add a "memory pool", which fixed this as a side effect.

regards

--
Tomas Vondra

Attachments:

results.pdfapplication/pdf; name=results.pdfDownload
In reply to: Tomas Vondra (#132)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Sat, May 10, 2025 at 10:59 AM Tomas Vondra <tomas@vondra.me> wrote:

But doesn't it also highlight how fragile this memory allocation is? The
skip scan patch didn't do anything wrong - it just added a couple
fields, using a little bit more memory. I think we understand allocating
more memory may need more time, but we expect the effect to be somewhat
proportional. Which doesn't seem to be the case here.

Many other patches add fields somewhere, it seems like bad luck the skip
scan happened to trigger this behavior. It's quite likely other patches
ran into the same issue, except that no one noticed. Maybe the skip scan
did that in much hotter code, not sure.

But what did the skip scan commit (specifically commit 92fe23d9,
without any of the follow-up commits) change about memory allocation,
that might be at issue with your test case? You said that that commit
"just added a couple fields". What specific fields are you talking
about, that were added by commit 92fe23d9?

I already speculated that the issue might be tied to the addition of a
new support routine (skip support), but the experiment we ran to try
to validate that theory disproved it. What else is there?

Again, commit 92fe23d9 didn't make BTScanOpaqueData any larger
(follow-up commit 8a510275 added a new BTScanOpaqueData.skipScan bool
field, but that didn't even affect sizeof BTScanOpaqueData, since the
field fit into what was previously just alignment padding space).
AFAICT nothing that seems like it might be relevant changed, apart
from the addition of the new support routine, which was ruled out
already.

--
Peter Geoghegan

#134Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#133)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 5/11/25 18:07, Peter Geoghegan wrote:

On Sat, May 10, 2025 at 10:59 AM Tomas Vondra <tomas@vondra.me> wrote:

But doesn't it also highlight how fragile this memory allocation is? The
skip scan patch didn't do anything wrong - it just added a couple
fields, using a little bit more memory. I think we understand allocating
more memory may need more time, but we expect the effect to be somewhat
proportional. Which doesn't seem to be the case here.

Many other patches add fields somewhere, it seems like bad luck the skip
scan happened to trigger this behavior. It's quite likely other patches
ran into the same issue, except that no one noticed. Maybe the skip scan
did that in much hotter code, not sure.

But what did the skip scan commit (specifically commit 92fe23d9,
without any of the follow-up commits) change about memory allocation,
that might be at issue with your test case? You said that that commit
"just added a couple fields". What specific fields are you talking
about, that were added by commit 92fe23d9?

I already speculated that the issue might be tied to the addition of a
new support routine (skip support), but the experiment we ran to try
to validate that theory disproved it. What else is there?

That's a good point. However, it seems I have done something wrong when
running the tests with the support routine removed :-( I just repeated
the tests, and I got this:

mode clients 3ba2cdaa454 master revert master revert
------------------------------------------------------------------
prepared 1 10860 3548 11048 33% 102%
4 25492 11299 25190 44% 99%
32 38399 14142 38493 37% 100%
------------------------------------------------------------------
simple 1 2595 1844 2604 71% 100%
4 8266 6090 8126 74% 98%
32 11765 7198 11449 61% 97%

I where "revert" is master with the removal patch. Sorry about the
confusion, I guess I was distracted and did some mistake.

So, this seems to be in line with the hypothesis ...

regards

--
Tomas Vondra

In reply to: Tomas Vondra (#134)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Sun, May 11, 2025 at 11:09 PM Tomas Vondra <tomas@vondra.me> wrote:

I where "revert" is master with the removal patch. Sorry about the
confusion, I guess I was distracted and did some mistake.

So, this seems to be in line with the hypothesis ...

That makes way more sense.

I wonder if we can fix this problem by getting rid of the old support
routine #5, "options". It currently doesn't do anything, and I always
thought it was strange that it was added "for uniformity" with other
index AMs.

OTOH, one could argue that it's only a matter of time until somebody
needs to add another support routine to nbtree; why delay dealing with
the problem that you've highlighted? Right now I don't really have an
opinion on how best to address the problem.

--
Peter Geoghegan

In reply to: Peter Geoghegan (#135)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Mon, May 12, 2025 at 8:58 AM Peter Geoghegan <pg@bowt.ie> wrote:

I wonder if we can fix this problem by getting rid of the old support
routine #5, "options". It currently doesn't do anything, and I always
thought it was strange that it was added "for uniformity" with other
index AMs.

Attached patch completely removes the nbtree "options" support
function, while changing the support function number of skip support:
it becomes support function #5 (the number previously used by
"options"). This patch should fix the regression that Tomas complained
about in an expedient way.

It's likely that somebody else will run into the same problem in the
future, the next time that a new support function is needed. But I
think that it makes sense to do this much now -- we need a short term
solution for Postgres 18. Usually I would never suggest breaking
compatibility like this, but, remarkably, we have never actually done
anything with our current support function 5. It's not possible to
break compatibility with code that can never be called in the first
place, so I see no compatibility to preserve.

Questions for Alexander about the "options" support function:

* Why did you invent the whole idea of an "options" support function,
given that it doesn't actually do anything? I get that it might be a
good idea to add these kinds of functions in the future, but why
didn't you wait until nbtree *actually had a use* for them?

* I've removed some of the tests that you added, that (for whatever
reason) cover nbtree specifically. The test from alter_generic.sql.
There might be some kind of loss of test coverage. What do you think?

--
Peter Geoghegan

Attachments:

v1-0001-Remove-OPTIONS-support-proc-from-nbtree.patchapplication/octet-stream; name=v1-0001-Remove-OPTIONS-support-proc-from-nbtree.patchDownload
From 2aec7ee8f5bfd3735769c6b29f4ac83ed538584f Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 20 May 2025 15:52:42 -0400
Subject: [PATCH v1] Remove OPTIONS support proc from nbtree.

---
 src/include/access/nbtree.h                 | 12 ++-----
 src/include/catalog/pg_amproc.dat           | 20 +++++------
 src/backend/access/nbtree/nbtree.c          |  2 +-
 src/backend/access/nbtree/nbtvalidate.c     |  3 --
 doc/src/sgml/btree.sgml                     | 37 ++-------------------
 doc/src/sgml/xindex.sgml                    | 15 +++------
 src/test/regress/expected/alter_generic.out | 25 +++-----------
 src/test/regress/expected/psql.out          |  2 +-
 src/test/regress/regress.c                  |  7 ----
 src/test/regress/sql/alter_generic.sql      | 20 ++---------
 10 files changed, 27 insertions(+), 116 deletions(-)

diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index ebca02588..8acc577f2 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -704,13 +704,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
  *	offer a forth amproc procedure (BTEQUALIMAGE_PROC).  For full details,
  *	see doc/src/sgml/btree.sgml.
  *
- *	An operator class may choose to offer a fifth amproc procedure
- *	(BTOPTIONS_PROC).  These procedures define a set of user-visible
- *	parameters that can be used to control operator class behavior.  None of
- *	the built-in B-Tree operator classes currently register an "options" proc.
- *
  *	To facilitate more efficient B-Tree skip scans, an operator class may
- *	choose to offer a sixth amproc procedure (BTSKIPSUPPORT_PROC).  For full
+ *	choose to offer a fifth amproc procedure (BTSKIPSUPPORT_PROC).  For full
  *	details, see src/include/utils/skipsupport.h.
  */
 
@@ -718,9 +713,8 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup)
 #define BTSORTSUPPORT_PROC	2
 #define BTINRANGE_PROC		3
 #define BTEQUALIMAGE_PROC	4
-#define BTOPTIONS_PROC		5
-#define BTSKIPSUPPORT_PROC	6
-#define BTNProcs			6
+#define BTSKIPSUPPORT_PROC	5
+#define BTNProcs			5
 
 /*
  *	We need to be able to tell the difference between read and write
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 925051489..fad086dc2 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -22,7 +22,7 @@
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '1', amproc => 'btboolcmp' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
-  amprocrighttype => 'bool', amprocnum => '6', amproc => 'btboolskipsupport' },
+  amprocrighttype => 'bool', amprocnum => '5', amproc => 'btboolskipsupport' },
 { amprocfamily => 'btree/bool_ops', amproclefttype => 'bool',
   amprocrighttype => 'bool', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
@@ -44,7 +44,7 @@
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/char_ops', amproclefttype => 'char',
-  amprocrighttype => 'char', amprocnum => '6', amproc => 'btcharskipsupport' },
+  amprocrighttype => 'char', amprocnum => '5', amproc => 'btcharskipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'date_cmp' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
@@ -52,7 +52,7 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
-  amprocrighttype => 'date', amprocnum => '6', amproc => 'date_skipsupport' },
+  amprocrighttype => 'date', amprocnum => '5', amproc => 'date_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'date',
   amprocrighttype => 'timestamp', amprocnum => '1',
   amproc => 'date_cmp_timestamp' },
@@ -67,7 +67,7 @@
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
-  amprocrighttype => 'timestamp', amprocnum => '6',
+  amprocrighttype => 'timestamp', amprocnum => '5',
   amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'date', amprocnum => '1', amproc => 'timestamp_cmp_date' },
@@ -84,7 +84,7 @@
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'btequalimage' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
-  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amprocrighttype => 'timestamptz', amprocnum => '5',
   amproc => 'timestamp_skipsupport' },
 { amprocfamily => 'btree/datetime_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'date', amprocnum => '1',
@@ -135,7 +135,7 @@
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
-  amprocrighttype => 'int2', amprocnum => '6', amproc => 'btint2skipsupport' },
+  amprocrighttype => 'int2', amprocnum => '5', amproc => 'btint2skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint24cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int2',
@@ -156,7 +156,7 @@
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
-  amprocrighttype => 'int4', amprocnum => '6', amproc => 'btint4skipsupport' },
+  amprocrighttype => 'int4', amprocnum => '5', amproc => 'btint4skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
   amprocrighttype => 'int8', amprocnum => '1', amproc => 'btint48cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int4',
@@ -177,7 +177,7 @@
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
-  amprocrighttype => 'int8', amprocnum => '6', amproc => 'btint8skipsupport' },
+  amprocrighttype => 'int8', amprocnum => '5', amproc => 'btint8skipsupport' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
   amprocrighttype => 'int4', amprocnum => '1', amproc => 'btint84cmp' },
 { amprocfamily => 'btree/integer_ops', amproclefttype => 'int8',
@@ -212,7 +212,7 @@
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
-  amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
+  amprocrighttype => 'oid', amprocnum => '5', amproc => 'btoidskipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -282,7 +282,7 @@
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/uuid_ops', amproclefttype => 'uuid',
-  amprocrighttype => 'uuid', amprocnum => '6', amproc => 'uuid_skipsupport' },
+  amprocrighttype => 'uuid', amprocnum => '5', amproc => 'uuid_skipsupport' },
 { amprocfamily => 'btree/record_ops', amproclefttype => 'record',
   amprocrighttype => 'record', amprocnum => '1', amproc => 'btrecordcmp' },
 { amprocfamily => 'btree/record_image_ops', amproclefttype => 'record',
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 765659887..1642c42c6 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -118,7 +118,7 @@ bthandler(PG_FUNCTION_ARGS)
 
 	amroutine->amstrategies = BTMaxStrategyNumber;
 	amroutine->amsupport = BTNProcs;
-	amroutine->amoptsprocnum = BTOPTIONS_PROC;
+	amroutine->amoptsprocnum = 0;
 	amroutine->amcanorder = true;
 	amroutine->amcanorderbyop = false;
 	amroutine->amcanhash = false;
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index 817ad358f..b673463fe 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -103,9 +103,6 @@ btvalidate(Oid opclassoid)
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
 											1, 1, OIDOID);
 				break;
-			case BTOPTIONS_PROC:
-				ok = check_amoptsproc_signature(procform->amproc);
-				break;
 			case BTSKIPSUPPORT_PROC:
 				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
 											1, 1, INTERNALOID);
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 027361f20..2e66323bd 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -207,7 +207,7 @@
 
  <para>
   As shown in <xref linkend="xindex-btree-support-table"/>, btree defines
-  one required and five optional support functions.  The six
+  one required and four optional support functions.  The five
   user-defined methods are:
  </para>
  <variablelist>
@@ -550,45 +550,12 @@ equalimage(<replaceable>opcintype</replaceable> <type>oid</type>) returns bool
     </para>
    </listitem>
   </varlistentry>
-  <varlistentry>
-   <term><function>options</function></term>
-   <listitem>
-    <para>
-     Optionally, a B-tree operator family may provide
-     <function>options</function> (<quote>operator class specific
-     options</quote>) support functions, registered under support
-     function number 5.  These functions define a set of user-visible
-     parameters that control operator class behavior.
-    </para>
-    <para>
-     An <function>options</function> support function must have the
-     signature
-<synopsis>
-options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns void
-</synopsis>
-     The function is passed a pointer to a <structname>local_relopts</structname>
-     struct, which needs to be filled with a set of operator class
-     specific options.  The options can be accessed from other support
-     functions using the <literal>PG_HAS_OPCLASS_OPTIONS()</literal> and
-     <literal>PG_GET_OPCLASS_OPTIONS()</literal> macros.
-    </para>
-    <para>
-     Currently, no B-Tree operator class has an <function>options</function>
-     support function.  B-tree doesn't allow flexible representation of keys
-     like GiST, SP-GiST, GIN and BRIN do.  So, <function>options</function>
-     probably doesn't have much application in the current B-tree index
-     access method.  Nevertheless, this support function was added to B-tree
-     for uniformity, and will probably find uses during further
-     evolution of B-tree in <productname>PostgreSQL</productname>.
-    </para>
-   </listitem>
-  </varlistentry>
   <varlistentry>
    <term><function>skipsupport</function></term>
    <listitem>
     <para>
      Optionally, a btree operator family may provide a <firstterm>skip
-      support</firstterm> function, registered under support function number 6.
+      support</firstterm> function, registered under support function number 5.
      These functions give the B-tree code a way to iterate through every
      possible value that can be represented by an operator class's underlying
      input type, in key space order.  This is used by the core code when it
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 7e23a7b6e..cda36ed70 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -454,19 +454,12 @@
        </entry>
        <entry>4</entry>
       </row>
-      <row>
-       <entry>
-        Define options that are specific to this operator class
-        (optional)
-       </entry>
-       <entry>5</entry>
-      </row>
       <row>
        <entry>
         Return the addresses of C-callable skip support function(s)
         (optional)
        </entry>
-       <entry>6</entry>
+       <entry>5</entry>
       </row>
      </tbody>
     </tgroup>
@@ -1070,7 +1063,7 @@ DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
   FUNCTION 2 btint8sortsupport(internal) ,
   FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
   FUNCTION 4 btequalimage(oid) ,
-  FUNCTION 6 btint8skipsupport(internal) ;
+  FUNCTION 5 btint8skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int4_ops
 DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
@@ -1084,7 +1077,7 @@ DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
   FUNCTION 2 btint4sortsupport(internal) ,
   FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
   FUNCTION 4 btequalimage(oid) ,
-  FUNCTION 6 btint4skipsupport(internal) ;
+  FUNCTION 5 btint4skipsupport(internal) ;
 
 CREATE OPERATOR CLASS int2_ops
 DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
@@ -1098,7 +1091,7 @@ DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
   FUNCTION 2 btint2sortsupport(internal) ,
   FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
   FUNCTION 4 btequalimage(oid) ,
-  FUNCTION 6 btint2skipsupport(internal) ;
+  FUNCTION 5 btint2skipsupport(internal) ;
 
 ALTER OPERATOR FAMILY integer_ops USING btree ADD
   -- cross-type comparisons int8 vs int2
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index 23bf33f10..03fa37f64 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -4,11 +4,6 @@
 -- directory paths and dlsuffix are passed to us in environment variables
 \getenv libdir PG_LIBDIR
 \getenv dlsuffix PG_DLSUFFIX
-\set regresslib :libdir '/regress' :dlsuffix
-CREATE FUNCTION test_opclass_options_func(internal)
-    RETURNS void
-    AS :'regresslib', 'test_opclass_options_func'
-    LANGUAGE C;
 -- Clean up in case a prior regression run failed
 SET client_min_messages TO 'warning';
 DROP ROLE IF EXISTS regress_alter_generic_user1;
@@ -362,9 +357,9 @@ ERROR:  invalid operator number 0, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ERROR:  operator argument types must be specified in ALTER OPERATOR FAMILY
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ERROR:  invalid function number 0, must be between 1 and 6
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
-ERROR:  invalid function number 7, must be between 1 and 6
+ERROR:  invalid function number 0, must be between 1 and 5
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
+ERROR:  invalid function number 6, must be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 ERROR:  STORAGE cannot be specified in ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
@@ -507,23 +502,11 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree
 ERROR:  ordering equal image functions must not be cross-type
 -- Should fail. Not allowed to have cross-type skip support function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
-  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+  ADD FUNCTION 5 (int4, int2) btint4skipsupport(internal);
 ERROR:  btree skip support functions must not be cross-type
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 ERROR:  function 2(integer,integer) does not exist in operator family "alt_opf18"
 DROP OPERATOR FAMILY alt_opf18 USING btree;
--- Should fail. Invalid opclass options function (#5) specifications.
-CREATE OPERATOR FAMILY alt_opf19 USING btree;
-ALTER OPERATOR FAMILY alt_opf19 USING btree ADD FUNCTION 5 test_opclass_options_func(internal, text[], bool);
-ERROR:  function test_opclass_options_func(internal, text[], boolean) does not exist
-ALTER OPERATOR FAMILY alt_opf19 USING btree ADD FUNCTION 5 (int4) btint42cmp(int4, int2);
-ERROR:  invalid operator class options parsing function
-HINT:  Valid signature of operator class options parsing function is (internal) RETURNS void.
-ALTER OPERATOR FAMILY alt_opf19 USING btree ADD FUNCTION 5 (int4, int2) btint42cmp(int4, int2);
-ERROR:  left and right associated data types for operator class options parsing functions must match
-ALTER OPERATOR FAMILY alt_opf19 USING btree ADD FUNCTION 5 (int4) test_opclass_options_func(internal); -- Ok
-ALTER OPERATOR FAMILY alt_opf19 USING btree DROP FUNCTION 5 (int4, int4);
-DROP OPERATOR FAMILY alt_opf19 USING btree;
 --
 -- Statistics
 --
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index cf48ae6d0..390dad39f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5332,7 +5332,7 @@ Function              | in_range(time without time zone,time without time zone,i
  btree | uuid_ops        | uuid                 | uuid                  |      1 | uuid_cmp
  btree | uuid_ops        | uuid                 | uuid                  |      2 | uuid_sortsupport
  btree | uuid_ops        | uuid                 | uuid                  |      4 | btequalimage
- btree | uuid_ops        | uuid                 | uuid                  |      6 | uuid_skipsupport
+ btree | uuid_ops        | uuid                 | uuid                  |      5 | uuid_skipsupport
  hash  | uuid_ops        | uuid                 | uuid                  |      1 | uuid_hash
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
 (6 rows)
diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c
index 3dbba0690..269c9d46e 100644
--- a/src/test/regress/regress.c
+++ b/src/test/regress/regress.c
@@ -803,13 +803,6 @@ test_support_func(PG_FUNCTION_ARGS)
 	PG_RETURN_POINTER(ret);
 }
 
-PG_FUNCTION_INFO_V1(test_opclass_options_func);
-Datum
-test_opclass_options_func(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_NULL();
-}
-
 /* one-time tests for encoding infrastructure */
 PG_FUNCTION_INFO_V1(test_enc_setup);
 Datum
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index 5e20dc633..eed49253e 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -6,13 +6,6 @@
 \getenv libdir PG_LIBDIR
 \getenv dlsuffix PG_DLSUFFIX
 
-\set regresslib :libdir '/regress' :dlsuffix
-
-CREATE FUNCTION test_opclass_options_func(internal)
-    RETURNS void
-    AS :'regresslib', 'test_opclass_options_func'
-    LANGUAGE C;
-
 -- Clean up in case a prior regression run failed
 SET client_min_messages TO 'warning';
 
@@ -310,7 +303,7 @@ ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 6 < (int4, int2); -- ope
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 0 < (int4, int2); -- operator number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD OPERATOR 1 < ; -- operator without argument types
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 0 btint42cmp(int4, int2); -- invalid options parsing function
-ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 7 btint42cmp(int4, int2); -- function number should be between 1 and 6
+ALTER OPERATOR FAMILY alt_opf4 USING btree ADD FUNCTION 6 btint42cmp(int4, int2); -- function number should be between 1 and 5
 ALTER OPERATOR FAMILY alt_opf4 USING btree ADD STORAGE invalid_storage; -- Ensure STORAGE is not a part of ALTER OPERATOR FAMILY
 DROP OPERATOR FAMILY alt_opf4 USING btree;
 
@@ -446,19 +439,10 @@ ALTER OPERATOR FAMILY alt_opf18 USING btree
   ADD FUNCTION 4 (int4, int2) btequalimage(oid);
 -- Should fail. Not allowed to have cross-type skip support function.
 ALTER OPERATOR FAMILY alt_opf18 USING btree
-  ADD FUNCTION 6 (int4, int2) btint4skipsupport(internal);
+  ADD FUNCTION 5 (int4, int2) btint4skipsupport(internal);
 ALTER OPERATOR FAMILY alt_opf18 USING btree DROP FUNCTION 2 (int4, int4);
 DROP OPERATOR FAMILY alt_opf18 USING btree;
 
--- Should fail. Invalid opclass options function (#5) specifications.
-CREATE OPERATOR FAMILY alt_opf19 USING btree;
-ALTER OPERATOR FAMILY alt_opf19 USING btree ADD FUNCTION 5 test_opclass_options_func(internal, text[], bool);
-ALTER OPERATOR FAMILY alt_opf19 USING btree ADD FUNCTION 5 (int4) btint42cmp(int4, int2);
-ALTER OPERATOR FAMILY alt_opf19 USING btree ADD FUNCTION 5 (int4, int2) btint42cmp(int4, int2);
-ALTER OPERATOR FAMILY alt_opf19 USING btree ADD FUNCTION 5 (int4) test_opclass_options_func(internal); -- Ok
-ALTER OPERATOR FAMILY alt_opf19 USING btree DROP FUNCTION 5 (int4, int4);
-DROP OPERATOR FAMILY alt_opf19 USING btree;
-
 --
 -- Statistics
 --
-- 
2.49.0

#137Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Peter Geoghegan (#136)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Tue, 20 May 2025, 22:14 Peter Geoghegan, <pg@bowt.ie> wrote:

On Mon, May 12, 2025 at 8:58 AM Peter Geoghegan <pg@bowt.ie> wrote:

I wonder if we can fix this problem by getting rid of the old support
routine #5, "options". It currently doesn't do anything, and I always
thought it was strange that it was added "for uniformity" with other
index AMs.

Attached patch completely removes the nbtree "options" support
function, while changing the support function number of skip support:
it becomes support function #5 (the number previously used by
"options"). This patch should fix the regression that Tomas complained
about in an expedient way.

It's likely that somebody else will run into the same problem in the
future, the next time that a new support function is needed. But I
think that it makes sense to do this much now -- we need a short term
solution for Postgres 18.

Didn't we have different solutions already? E.g. dropping the
allocation requirement from IndexAM (and indeed, making it
non-required), as seen in my 0001? It's a bit of a hassle, but has
shown to have the same effect in PG18b1, doesn't break compatibility
with existing amproc registrations, and even improves the memory
situation for all other index types.

Usually I would never suggest breaking
compatibility like this, but, remarkably, we have never actually done
anything with our current support function 5. It's not possible to
break compatibility with code that can never be called in the first
place, so I see no compatibility to preserve.

I disagree. An extension or user may well have registered FUNCTION 5
for their opclass or opfamily as blanket future-proofed
implementation, which will now break after pg_upgrade. We never
rejected this registration before, and while there won't be issues
with the proc signature across the versions (options' expected
signature is equivalent to that of skipsupport), it will probably
cause issues when it's called with unexpected inputs.

Kind regards,

Matthias van de Meent
Neon (https://neon.tech)

In reply to: Peter Geoghegan (#105)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, May 2, 2025 at 3:04 PM Peter Geoghegan <pg@bowt.ie> wrote:

The second patch is more complicated, and seems like something that
I'll need to spend more time thinking about before proceeding with
commit. It has subtle behavioral implications, in that it causes the
pstate.forcenonrequired mechanism to influence when and how
_bt_advance_array_keys schedules primitive index scans in a tiny
minority of forward scan cases.

While I have no reason to believe that there were any problems in this
bugfix (which became commit 5f4d98d4), there were problems in the
follow-up, commit 54c6ea8c, "nbtree: Remove useless row compare arg".

I devised a new test case (which is too large to be easily posted to
this list) that shows a RowCompare query that fails with an assertion
failure (the assert added by 54c6ea8c). Here's the query:

select a, b, c, d
from fuzz_skip_scan
where b is not null and (c, d) < (60, 0)
order by a, b, c, d
limit 200 offset 80_000;

There is at least one page where _bt_set_startikey will want to apply
forcenonrequired mode while setting pstate.ikey = 1 (meaning that we
can start _bt_checkkeys calls with the "b" scan key, avoiding
maintenance of the "a" scan key). So clearly we do need support for
forcenonrequired=true + a RowCompare key. The row compare arg wasn't
so useless after all.

With commit 5f4d98d4 in place and commit 54c6ea8c reverted, everything
works here. The row compare on "(c, d)" shouldn't need to prevent
application of pstate.forcenonrequired mode here. The skip array on
"a" is bound to advance on the page anyway, so there's no risk that
we'll do the wrong thing with the RowCompare for the pstate.finaltup
call to _bt_checkkeys, that takes place after the scan's arrays have
been reset.

I'm going to revert my ill-advised commit 54c6ea8c now.

I should give this general area more thought. Some comment updates in
_bt_set_startikey seem in order, at least. But I see no reason to wait
to revert.

--
Peter Geoghegan

#139Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Matthias van de Meent (#137)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Hi,

On Thu, 22 May 2025 at 19:57, Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:

On Tue, 20 May 2025, 22:14 Peter Geoghegan, <pg@bowt.ie> wrote:

On Mon, May 12, 2025 at 8:58 AM Peter Geoghegan <pg@bowt.ie> wrote:

I wonder if we can fix this problem by getting rid of the old support
routine #5, "options". It currently doesn't do anything, and I always
thought it was strange that it was added "for uniformity" with other
index AMs.

Attached patch completely removes the nbtree "options" support
function, while changing the support function number of skip support:
it becomes support function #5 (the number previously used by
"options"). This patch should fix the regression that Tomas complained
about in an expedient way.

It's likely that somebody else will run into the same problem in the
future, the next time that a new support function is needed. But I
think that it makes sense to do this much now -- we need a short term
solution for Postgres 18.

I just realized I hadn't checked in on this in a while, and I haven't
seen Peter's patch get committed, nor my 0001. Do we consider this an
Open Item and should this be improved in PG18, or is this something
the user is expected to figure out and configure their systems for?

If we want to fix it let's make a decision before RC1, so we don't
have further breaking catalog changes between RC1 and 18.0.

cc-ed RMT as this might be Open Item-worthy, and the patches up for
debate both change catalog behaviour.

Peter's patch at [0]/messages/by-id/CAH2-Wzkk8k_7wj8UUhYb2=q_8D=-c1mtwuG4PCb7j+SNEtD3Ew@mail.gmail.com changes opclass procedure numbers to reuse an
existing but unused options regproc number.
My 0001 at [1]/messages/by-id/CAEze2Wi7tDidbDVJhu=Pstb2hbUXDCxx_VAZnKSqbTMf7k8+uQ@mail.gmail.com changes the memory residence status of index access
methods' handler_function output to const static, from dynamic in
memctx.

Kind regards,

Matthias van de Meent

[0]: /messages/by-id/CAH2-Wzkk8k_7wj8UUhYb2=q_8D=-c1mtwuG4PCb7j+SNEtD3Ew@mail.gmail.com
[1]: /messages/by-id/CAEze2Wi7tDidbDVJhu=Pstb2hbUXDCxx_VAZnKSqbTMf7k8+uQ@mail.gmail.com

#140Tomas Vondra
tomas@vondra.me
In reply to: Matthias van de Meent (#139)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 8/29/25 10:38, Matthias van de Meent wrote:

Hi,

On Thu, 22 May 2025 at 19:57, Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:

On Tue, 20 May 2025, 22:14 Peter Geoghegan, <pg@bowt.ie> wrote:

On Mon, May 12, 2025 at 8:58 AM Peter Geoghegan <pg@bowt.ie> wrote:

I wonder if we can fix this problem by getting rid of the old support
routine #5, "options". It currently doesn't do anything, and I always
thought it was strange that it was added "for uniformity" with other
index AMs.

Attached patch completely removes the nbtree "options" support
function, while changing the support function number of skip support:
it becomes support function #5 (the number previously used by
"options"). This patch should fix the regression that Tomas complained
about in an expedient way.

It's likely that somebody else will run into the same problem in the
future, the next time that a new support function is needed. But I
think that it makes sense to do this much now -- we need a short term
solution for Postgres 18.

I just realized I hadn't checked in on this in a while, and I haven't
seen Peter's patch get committed, nor my 0001. Do we consider this an
Open Item and should this be improved in PG18, or is this something
the user is expected to figure out and configure their systems for?

If we want to fix it let's make a decision before RC1, so we don't
have further breaking catalog changes between RC1 and 18.0.

The thing is that if we want to make this to RC1, it needs to go in
*today*. The RC1 is planned for next week, and there's a freeze starting
tomorrow.

cc-ed RMT as this might be Open Item-worthy, and the patches up for
debate both change catalog behaviour.

I agree with this, personally. Maybe the other RMT members will see it
differently, but it's probably to add an open item and then remove it
than miss an issue.

Peter's patch at [0] changes opclass procedure numbers to reuse an
existing but unused options regproc number.
My 0001 at [1] changes the memory residence status of index access
methods' handler_function output to const static, from dynamic in
memctx.

IIRC both approaches address the issue. I'd go with Peter's patch for
18. The other patch is much more invasive / bigger, and we're right
before RC1 freeze. Maybe it's a good idea, but I'd say it's for 19.

Peter, any thoughts on this. Do you think it's reasonable / feasible to
push the fix?

regards

--
Tomas Vondra

In reply to: Tomas Vondra (#140)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Fri, Aug 29, 2025 at 9:10 AM Tomas Vondra <tomas@vondra.me> wrote:

Peter, any thoughts on this. Do you think it's reasonable / feasible to
push the fix?

I don't feel comfortable pushing that fix today.

Honestly, I'm still not sure what to do. My proposal was to just
remove the totally unused options support function, which is probably
fine. But since I don't really know why Alexander ever added the
"options" support function in the first place (I don't even see a
theoretical benefit), I'm not quite prepared to say that I know that
it's okay to remove it now.

--
Peter Geoghegan

#142Tomas Vondra
tomas@vondra.me
In reply to: Peter Geoghegan (#141)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On 8/29/25 21:03, Peter Geoghegan wrote:

On Fri, Aug 29, 2025 at 9:10 AM Tomas Vondra <tomas@vondra.me> wrote:

Peter, any thoughts on this. Do you think it's reasonable / feasible to
push the fix?

I don't feel comfortable pushing that fix today.

Understood.

Honestly, I'm still not sure what to do. My proposal was to just
remove the totally unused options support function, which is probably
fine. But since I don't really know why Alexander ever added the
"options" support function in the first place (I don't even see a
theoretical benefit), I'm not quite prepared to say that I know that
it's okay to remove it now.

Right. I think removing the "options" is the only feasible solution for
PG18 at this point. Either that or nothing. The other patch is far too
invasive.

As for why the support procedure was added to existing index AMs, I
don't know. I suppose it as mostly for consistency, so that custom
oclasses could opclasses could use that. I have no idea if there are
plausible custom opclasses using this.

I'm not sure how I feel about removing the support proc. It feels pretty
arbitrary and fragile, and IIRC it doesn't even address the perf issue
(add a couple partitions and it'll hit the same issue). It just restores
the "threshold" to where it was for PG17. And it's fragile, because we
have no protections about hitting this glibc-specific behavior again. It
takes one new flag added somewhere, and we'll not even notice it.

So after thinking about this a bit more, and refreshing the context, I
think the right solution for PG18 is to do nothing.

regards

--
Tomas Vondra

#143BharatDB
bharatdbpg@gmail.com
In reply to: Tomas Vondra (#142)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Hi Team,

As a follow-up to the skip scan regression discussion, I tested a small
patch that introduces *static allocation/caching of `IndexAmRoutine` *objects
in `amapi.c`, removing the malloc/free overhead.

*Test setup :*
- Baseline: PG17 (commit before skip scan)
- After: PG18 build with skip scan (patched)
- pgbench scale=1, 100 partitions
- Query: `select count(*) from pgbench_accounts where bid = 0`
- Clients: 1, 4, 32
- Protocols: simple, prepared

*Results (tps, 10s runs) :*

Mode Clients Before (PG17) After (PG18 w/ static fix)

simple 1 23856 20332 (~15% lower)
simple 4 55299 53184 (~4% lower)
simple 32 79779 78347 (~2% lower)

prepared 1 26364 26615 (no regression)
prepared 4 55784 54437 (~2% lower)
prepared 32 84687 80374 (~5% lower)

This shows the static fix eliminates the severe ~50% regression previously
observed by Tomas, leaving only a small residual slowdown (*~2-15%*).

*Patch summary :*
- Cache `IndexAmRoutine` instances per AM OID instead of malloc/free per
call.
- Avoid `pfree(amroutine)` in hot paths.
- Keeps allocations stable across lookups, reducing malloc churn.

*Proposal :*
I suggest adopting this static allocation approach for PG18 to prevent
performance cliffs. Longer term, we can explore lighter-weight caching
mechanisms or further executor tuning.

*Patch attached for discussion.*

Thanks & Regards,
Athiyaman M

On Sat, Aug 30, 2025 at 4:37 AM Tomas Vondra <tomas@vondra.me> wrote:

Show quoted text

On 8/29/25 21:03, Peter Geoghegan wrote:

On Fri, Aug 29, 2025 at 9:10 AM Tomas Vondra <tomas@vondra.me> wrote:

Peter, any thoughts on this. Do you think it's reasonable / feasible to
push the fix?

I don't feel comfortable pushing that fix today.

Understood.

Honestly, I'm still not sure what to do. My proposal was to just
remove the totally unused options support function, which is probably
fine. But since I don't really know why Alexander ever added the
"options" support function in the first place (I don't even see a
theoretical benefit), I'm not quite prepared to say that I know that
it's okay to remove it now.

Right. I think removing the "options" is the only feasible solution for
PG18 at this point. Either that or nothing. The other patch is far too
invasive.

As for why the support procedure was added to existing index AMs, I
don't know. I suppose it as mostly for consistency, so that custom
oclasses could opclasses could use that. I have no idea if there are
plausible custom opclasses using this.

I'm not sure how I feel about removing the support proc. It feels pretty
arbitrary and fragile, and IIRC it doesn't even address the perf issue
(add a couple partitions and it'll hit the same issue). It just restores
the "threshold" to where it was for PG17. And it's fragile, because we
have no protections about hitting this glibc-specific behavior again. It
takes one new flag added somewhere, and we'll not even notice it.

So after thinking about this a bit more, and refreshing the context, I
think the right solution for PG18 is to do nothing.

regards

--
Tomas Vondra

Attachments:

0001-Use-static-allocation-for-IndexAmRoutine-in-amapi.c-.patchtext/x-patch; charset=US-ASCII; name=0001-Use-static-allocation-for-IndexAmRoutine-in-amapi.c-.patchDownload
From c80b2a07733112e23b70ed5b309f177f6bf79ef6 Mon Sep 17 00:00:00 2001
From: athiyaman-m <heroathi303@gmail.com>
Date: Wed, 10 Sep 2025 12:08:56 +0530
Subject: [PATCH] Use static allocation for IndexAmRoutine in amapi.c to reduce
 malloc/free overhead

This avoids repeated palloc/free cycles by keeping the routine
structure in static memory. Initial pgbench tests show that this
does not regress performance and may slightly improve efficiency
at higher client counts.

Signed-off-by: athiyaman-m <heroathi303@gmail.com>
---
 src/backend/access/index/amapi.c | 81 +++++++++++++++++++++++++++++---
 1 file changed, 75 insertions(+), 6 deletions(-)

diff --git a/src/backend/access/index/amapi.c b/src/backend/access/index/amapi.c
index d6b8dad4d5..d5481569d9 100644
--- a/src/backend/access/index/amapi.c
+++ b/src/backend/access/index/amapi.c
@@ -13,12 +13,19 @@
  */
 #include "postgres.h"
 
+#include <string.h>
+
 #include "access/amapi.h"
 #include "access/htup_details.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_opclass.h"
 #include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
+#include "utils/hsearch.h"
+#include "utils/memutils.h"
+#include "utils/elog.h"
+#include "utils/pg_locale.h"
+#include "utils/builtins.h"
 
 
 /*
@@ -51,6 +58,10 @@ GetIndexAmRoutine(Oid amhandler)
  *
  * If the given OID isn't a valid index access method, returns NULL if
  * noerror is true, else throws error.
+ *
+ * This implementation caches a copy of the IndexAmRoutine per handler OID
+ * in TopMemoryContext so callers get a stable pointer and we avoid repeated
+ * per-call allocations/freeing of the routine struct.
  */
 IndexAmRoutine *
 GetIndexAmRoutineByAmId(Oid amoid, bool noerror)
@@ -65,8 +76,7 @@ GetIndexAmRoutineByAmId(Oid amoid, bool noerror)
 	{
 		if (noerror)
 			return NULL;
-		elog(ERROR, "cache lookup failed for access method %u",
-			 amoid);
+		elog(ERROR, "cache lookup failed for access method %u", amoid);
 	}
 	amform = (Form_pg_am) GETSTRUCT(tuple);
 
@@ -102,8 +112,67 @@ GetIndexAmRoutineByAmId(Oid amoid, bool noerror)
 
 	ReleaseSysCache(tuple);
 
-	/* And finally, call the handler function to get the API struct. */
-	return GetIndexAmRoutine(amhandler);
+	/*
+	 * Cache management: keep a single HTAB (in TopMemoryContext) that maps
+	 * amhandler OIDs to a stored copy of IndexAmRoutine.
+	 *
+	 * Use HASH_BLOBS and store the copy of IndexAmRoutine inline in the entry.
+	 */
+	{
+		typedef struct IndexAmRoutineCacheEntry
+		{
+			IndexAmRoutine routine;	/* stored inline */
+		} IndexAmRoutineCacheEntry;
+
+		static HTAB *IndexAmRoutine_cache = NULL;
+
+		if (IndexAmRoutine_cache == NULL)
+		{
+			HASHCTL ctl;
+
+			/* initialize hash control structure */
+			MemSet(&ctl, 0, sizeof(ctl));
+			ctl.keysize = sizeof(Oid);
+			ctl.entrysize = sizeof(IndexAmRoutineCacheEntry);
+			ctl.hcxt = TopMemoryContext;
+
+			/* create cache in TopMemoryContext; small fixed initial size */
+			IndexAmRoutine_cache = hash_create("IndexAmRoutine cache",
+											   8,
+											   &ctl,
+											   HASH_ELEM | HASH_BLOBS);
+		}
+
+		/* call handler to get the runtime-provided struct (may be palloc'd) */
+		IndexAmRoutine *runtime_routine = GetIndexAmRoutine(amhandler);
+		bool		found;
+		IndexAmRoutineCacheEntry *entry;
+
+		/* find or create entry; key is the amhandler Oid */
+		entry = (IndexAmRoutineCacheEntry *) hash_search(IndexAmRoutine_cache,
+														 (void *)&amhandler,
+														 HASH_ENTER,
+														 &found);
+		if (!found)
+		{
+			/* copy runtime struct into persistent cache entry */
+			/* ensure we don't copy past the size of IndexAmRoutine */
+			memcpy(&entry->routine, runtime_routine, sizeof(IndexAmRoutine));
+		}
+
+		/*
+		 * Note: we intentionally do not pfree(runtime_routine) here. The
+		 * handler may have returned a pointer into static memory or memory
+		 * allocated in some context. Freeing it blindly may be unsafe. The
+		 * runtime allocation (if any) will be at most one small struct per
+		 * handler and is acceptable for this prototype. A more polished
+		 * implementation could detect and free an allocated pointer when
+		 * safe.
+		 */
+
+		/* return pointer to the cached copy (in TopMemoryContext) */
+		return &entry->routine;
+	}
 }
 
 
@@ -187,7 +256,7 @@ amvalidate(PG_FUNCTION_ARGS)
 
 	result = amroutine->amvalidate(opclassoid);
 
-	pfree(amroutine);
+	/* Previously we pfree(amroutine); but routine is now cached in TopMemoryContext. */
 
 	PG_RETURN_BOOL(result);
-}
+}
\ No newline at end of file
-- 
2.39.5

#144BharatDB
bharatdbpg@gmail.com
In reply to: BharatDB (#143)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Dear Team,

With reference to the conversation ongoing in message ID :
c562dc2a-6e36-46f3-a5ea-cd42eebd7118,
As a follow-up to the skip scan regression discussion, I tested a small
patch that introduces *static allocation/caching of `IndexAmRoutine` *objects
in `amapi.c`, removing the malloc/free overhead.

*Test setup :*
- Baseline: PG17 (commit before skip scan)
- After: PG18 build with skip scan (patched)
- pgbench scale=1, 100 partitions
- Query: `select count(*) from pgbench_accounts where bid = 0`
- Clients: 1, 4, 32
- Protocols: simple, prepared

*Results (tps, 10s runs) :*

Mode Clients Before (PG17) After (PG18 w/ static fix)

simple 1 23856 20332 (~15% lower)
simple 4 55299 53184 (~4% lower)
simple 32 79779 78347 (~2% lower)

prepared 1 26364 26615 (no regression)
prepared 4 55784 54437 (~2% lower)
prepared 32 84687 80374 (~5% lower)

This shows the static fix eliminates the severe ~50% regression previously
observed by Tomas, leaving only a small residual slowdown (*~2-15%*).

*Patch summary :*
- Cache `IndexAmRoutine` instances per AM OID instead of malloc/free per
call.
- Avoid `pfree(amroutine)` in hot paths.
- Keeps allocations stable across lookups, reducing malloc churn.

*Proposal :*
I suggest adopting this static allocation approach for PG18 to prevent
performance cliffs. Longer term, we can explore lighter-weight caching
mechanisms or further executor tuning.

*Patch attached for discussion.*

Thanks & Regards,
Athiyaman M

Attachments:

0001-Use-static-allocation-for-IndexAmRoutine-in-amapi.c-.patchtext/x-patch; charset=US-ASCII; name=0001-Use-static-allocation-for-IndexAmRoutine-in-amapi.c-.patchDownload
From c80b2a07733112e23b70ed5b309f177f6bf79ef6 Mon Sep 17 00:00:00 2001
From: athiyaman-m <heroathi303@gmail.com>
Date: Wed, 10 Sep 2025 12:08:56 +0530
Subject: [PATCH] Use static allocation for IndexAmRoutine in amapi.c to reduce
 malloc/free overhead

This avoids repeated palloc/free cycles by keeping the routine
structure in static memory. Initial pgbench tests show that this
does not regress performance and may slightly improve efficiency
at higher client counts.

Signed-off-by: athiyaman-m <heroathi303@gmail.com>
---
 src/backend/access/index/amapi.c | 81 +++++++++++++++++++++++++++++---
 1 file changed, 75 insertions(+), 6 deletions(-)

diff --git a/src/backend/access/index/amapi.c b/src/backend/access/index/amapi.c
index d6b8dad4d5..d5481569d9 100644
--- a/src/backend/access/index/amapi.c
+++ b/src/backend/access/index/amapi.c
@@ -13,12 +13,19 @@
  */
 #include "postgres.h"
 
+#include <string.h>
+
 #include "access/amapi.h"
 #include "access/htup_details.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_opclass.h"
 #include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
+#include "utils/hsearch.h"
+#include "utils/memutils.h"
+#include "utils/elog.h"
+#include "utils/pg_locale.h"
+#include "utils/builtins.h"
 
 
 /*
@@ -51,6 +58,10 @@ GetIndexAmRoutine(Oid amhandler)
  *
  * If the given OID isn't a valid index access method, returns NULL if
  * noerror is true, else throws error.
+ *
+ * This implementation caches a copy of the IndexAmRoutine per handler OID
+ * in TopMemoryContext so callers get a stable pointer and we avoid repeated
+ * per-call allocations/freeing of the routine struct.
  */
 IndexAmRoutine *
 GetIndexAmRoutineByAmId(Oid amoid, bool noerror)
@@ -65,8 +76,7 @@ GetIndexAmRoutineByAmId(Oid amoid, bool noerror)
 	{
 		if (noerror)
 			return NULL;
-		elog(ERROR, "cache lookup failed for access method %u",
-			 amoid);
+		elog(ERROR, "cache lookup failed for access method %u", amoid);
 	}
 	amform = (Form_pg_am) GETSTRUCT(tuple);
 
@@ -102,8 +112,67 @@ GetIndexAmRoutineByAmId(Oid amoid, bool noerror)
 
 	ReleaseSysCache(tuple);
 
-	/* And finally, call the handler function to get the API struct. */
-	return GetIndexAmRoutine(amhandler);
+	/*
+	 * Cache management: keep a single HTAB (in TopMemoryContext) that maps
+	 * amhandler OIDs to a stored copy of IndexAmRoutine.
+	 *
+	 * Use HASH_BLOBS and store the copy of IndexAmRoutine inline in the entry.
+	 */
+	{
+		typedef struct IndexAmRoutineCacheEntry
+		{
+			IndexAmRoutine routine;	/* stored inline */
+		} IndexAmRoutineCacheEntry;
+
+		static HTAB *IndexAmRoutine_cache = NULL;
+
+		if (IndexAmRoutine_cache == NULL)
+		{
+			HASHCTL ctl;
+
+			/* initialize hash control structure */
+			MemSet(&ctl, 0, sizeof(ctl));
+			ctl.keysize = sizeof(Oid);
+			ctl.entrysize = sizeof(IndexAmRoutineCacheEntry);
+			ctl.hcxt = TopMemoryContext;
+
+			/* create cache in TopMemoryContext; small fixed initial size */
+			IndexAmRoutine_cache = hash_create("IndexAmRoutine cache",
+											   8,
+											   &ctl,
+											   HASH_ELEM | HASH_BLOBS);
+		}
+
+		/* call handler to get the runtime-provided struct (may be palloc'd) */
+		IndexAmRoutine *runtime_routine = GetIndexAmRoutine(amhandler);
+		bool		found;
+		IndexAmRoutineCacheEntry *entry;
+
+		/* find or create entry; key is the amhandler Oid */
+		entry = (IndexAmRoutineCacheEntry *) hash_search(IndexAmRoutine_cache,
+														 (void *)&amhandler,
+														 HASH_ENTER,
+														 &found);
+		if (!found)
+		{
+			/* copy runtime struct into persistent cache entry */
+			/* ensure we don't copy past the size of IndexAmRoutine */
+			memcpy(&entry->routine, runtime_routine, sizeof(IndexAmRoutine));
+		}
+
+		/*
+		 * Note: we intentionally do not pfree(runtime_routine) here. The
+		 * handler may have returned a pointer into static memory or memory
+		 * allocated in some context. Freeing it blindly may be unsafe. The
+		 * runtime allocation (if any) will be at most one small struct per
+		 * handler and is acceptable for this prototype. A more polished
+		 * implementation could detect and free an allocated pointer when
+		 * safe.
+		 */
+
+		/* return pointer to the cached copy (in TopMemoryContext) */
+		return &entry->routine;
+	}
 }
 
 
@@ -187,7 +256,7 @@ amvalidate(PG_FUNCTION_ARGS)
 
 	result = amroutine->amvalidate(opclassoid);
 
-	pfree(amroutine);
+	/* Previously we pfree(amroutine); but routine is now cached in TopMemoryContext. */
 
 	PG_RETURN_BOOL(result);
-}
+}
\ No newline at end of file
-- 
2.39.5

#145Natalya Aksman
natalya@tigerdata.com
In reply to: Peter Geoghegan (#47)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

May I suggest a fix/improvement regarding "so->skipScan = false;" being
set in "btbeginscan":
https://github.com/postgres/postgres/blob/b8a1bdc458e3e81898a1fe3d26188bc1dcbac965/src/backend/access/nbtree/nbtree.c#L356
.

It should also be reset in "btrescan": i.e. when we rescan the index, we
should also reset "so->skipScan = false".

Our Timescaledb extension has scenarios changing ">" quals to "=" and back
on rescan and it breaks when so->Skipscan needs to be reset from true to
false.
I'm not aware of any such scenarios in regular PG but they might come up in
the future. It's a small and safe tweak it seems, hopefully it can get into
PG18 release.

Thank you,
Natalya Aksman
Senior Software Engineer, TigerData

On Wed, Sep 10, 2025 at 9:34 AM Peter Geoghegan <pg@bowt.ie> wrote:

Show quoted text

Hi Masahiro,

On Tue, Nov 19, 2024 at 3:30 AM Masahiro Ikeda <ikedamsh@oss.nttdata.com>
wrote:

Apologies for the delayed response. I've confirmed that the costing is
significantly
improved for multicolumn indexes in the case I provided. Thanks!

/messages/by-id/TYWPR01MB10982A413E0EC4088E78C0E11B1A62@TYWPR01MB10982.jpnprd01.prod.outlook.com

Great! I made it one of my private/internal test cases for the
costing. Your test case was quite helpful.

Attached is v15. It works through your feedback.

Importantly, v15 has a new patch which has a fix for your test.sql
case -- which is the most important outstanding problem with the patch
(and has been for a long time now). I've broken those changes out into
a separate patch because they're still experimental, and have some
known minor bugs. But it works well enough for you to assess how close
I am to satisfactorily fixing the known regressions, so it seems worth
posting quickly.

IIUC, why not add it to the documentation? It would clearly help users
understand how to tune their queries using the counter, and it would
also show that the counter is not just for developers.

The documentation definitely needs more work. I have a personal TODO
item about that.

Changes to the documentation can be surprisingly contentious, so I
often work on it last, when we have the clearest picture of how to
talk about the feature. For example, Matthias said something that's
approximately the opposite of what you said about it (though I agree
with you about it).

From the perspective of consistency, wouldn't it be better to align the
naming
between the EXPLAIN output and pg_stat_all_indexes.idx_scan, even though
the
documentation states they refer to the same concept?

I personally prefer something like "search" instead of "scan", as "scan"
is
commonly associated with node names like Index Scan and similar terms.
To maintain
consistency, how about renaming pg_stat_all_indexes.idx_scan to
pg_stat_all_indexes.idx_search?

I suspect that other hackers will reject that proposal on
compatibility grounds, even though it would make sense in a "green
field" situation.

Honestly, discussions about UI/UX details such as EXPLAIN ANALYZE
always tend to result in unproductive bikeshedding. What I really want
is something that will be acceptable to all parties. I don't have any
strong opinions of my own about it -- I just think that it's important
to show *something* like "Index Searches: N" to make skip scan user
friendly.

(3)

v14-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patch

The counter should be added in blgetbitmap().

Fixed.

(4)

v14-0001-Show-index-search-count-in-EXPLAIN-ANALYZE.patch
doc/src/sgml/bloom.sgml

The below forgot "Index Searches: 1".

-&gt; Bitmap Index Scan on btreeidx2 (cost=0.00..12.04
rows=500 width=0) (never executed)
Index Cond: (i2 = 898732)
Planning Time: 0.491 ms
Execution Time: 0.055 ms
(10 rows)

Fixed (though I made it show "Index Searches: 0" instead, since this
particular index scan node is "never executed").

Although we may not need to fix it, due to the support for skip scan,
the B-tree
index is now selected over the Bloom index in my environment.

I am not inclined to change it.

Although I tested with various data types such as int, uuid, oid, and
others on my
local PC, I could only identify the regression case that you already
mentioned.

That's good news!

Although it's not an optimal solution and would only reduce the degree
of performance
degradation, how about introducing a threshold per page to switch from
skip scan to full
index scan?

The approach to fixing these regressions from the new experimental
patch doesn't need to use any such threshold. It is effective both
with simple "WHERE id2 = 100" cases (like the queries from your
test.sql test case), as well as more complicated "WHERE id2 BETWEEN 99
AND 101" inequality cases.

What do you think? The regressions are easily under 5% with the new
patch applied, which is in the noise.

At the same time, we're just as capable of skipping whenever the scan
encounters a large group of skipped-prefix-column duplicates. For
example, if I take your test.sql test case and add another insert that
adds such a group (e.g., "INSERT INTO t SELECT 55, i FROM
generate_series(-1000000, 1000000) i;" ), and then re-run the query,
the scan is exactly as fast as before -- it just skips to get over the
newly inserted "55" group of tuples. Obviously, this also makes the
master branch far, far slower.

As I've said many times already, the need to be flexible and offer
robust performance in cases where skipping is either very effective or
very ineffective *during the same index scan* seems very important to
me. This "55" variant of your test.sql test case is a great example of
the kinds of cases I was thinking about.

Is it better to move prev_numSkipArrayKeys =*numSkipArrayKeys after the
while loop?
For example, the index below should return *numSkipArrayKeys = 0 instead
of 1
if the id3 type does not support eq_op.

* index: CREATE INDEX test_idx on TEST (id1 int, id2 int, id3 no_eq_op,
id4 int);
* query: SELECT * FROM test WHERE id4 = 10;

Nice catch! You're right. Fixed this in v15, too.

Thanks for the review
--
Peter Geoghegan

In reply to: Natalya Aksman (#145)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Sep 10, 2025 at 9:53 AM Natalya Aksman <natalya@tigerdata.com> wrote:

Our Timescaledb extension has scenarios changing ">" quals to "=" and back on rescan and it breaks when so->Skipscan needs to be reset from true to false.

But the amrescan docs say:

"In practice the restart feature is used when a new outer tuple is
selected by a nested-loop join and so a new key comparison value is
needed, but the scan key structure remains the same" [1]https://www.postgresql.org/docs/current/index-functions.html -- Peter Geoghegan.

I don't understand why it is that our not resetting the so->Skipscan
flag within btrescan has any particular significance to Timescaledb,
relative to all of the other fields that are supposed to be set by
_bt_preprocess_keys. What is the actual failure you see? Is it an
assertion failure within _bt_readpage/_bt_checkkeys?

Note that btrescan *does* set "so->numberOfKeys = 0", which will make
the next call to _bt_preprocess_keys (from _bt_first) perform
preprocessing from scratch. This should set so->Skipscan from scratch
on each rescan (along with every other field set by preprocessing). It
seems like that should work for you (in spite of the fact that you're
doing something that seems at odds with the index AM API).

[1]: https://www.postgresql.org/docs/current/index-functions.html -- Peter Geoghegan
--
Peter Geoghegan

In reply to: Peter Geoghegan (#146)
1 attachment(s)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Sep 10, 2025 at 12:45 PM Peter Geoghegan <pg@bowt.ie> wrote:

I don't understand why it is that our not resetting the so->Skipscan
flag within btrescan has any particular significance to Timescaledb,
relative to all of the other fields that are supposed to be set by
_bt_preprocess_keys.

I notice that the skipScan flag isn't initialized in the path where
there's no array keys at all on this rescan.

Does the attached patch (which moves back the initialization such that
the flag will always be initialized on rescan) fix the problem you're
seeing?

What is the actual failure you see? Is it an
assertion failure within _bt_readpage/_bt_checkkeys?

Still curious about this.

--
Peter Geoghegan

Attachments:

0001-Initialize-skipScan-field-consistently.patchapplication/octet-stream; name=0001-Initialize-skipScan-field-consistently.patchDownload
From ab1f13964b280710ad969ff67c2f2c06a288d588 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Wed, 10 Sep 2025 12:52:10 -0400
Subject: [PATCH] Initialize skipScan field consistently

---
 src/backend/access/nbtree/nbtpreprocesskeys.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 936b93f15..74dbfc85f 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -1842,6 +1842,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	 * (also checks if we should add extra skip arrays based on input keys)
 	 */
 	numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys);
+	so->skipScan = (numSkipArrayKeys > 0);
 
 	/* Quit if nothing to do. */
 	if (numArrayKeys == 0)
@@ -1871,7 +1872,6 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
 	arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData));
 
 	/* Allocate space for per-array data in the workspace context */
-	so->skipScan = (numSkipArrayKeys > 0);
 	so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo));
 
 	/* Allocate space for ORDER procs used to help _bt_checkkeys */
-- 
2.51.0

#148Natalya Aksman
natalya@tigerdata.com
In reply to: Peter Geoghegan (#146)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Timescaledb implemented multikey skipscan feature for queries like "select
distinct key1, key2 ... from t_indexed_on_key1_key2". It pins key1 to a
found key value (i.e key1=val1) to skip over distinct values of key2. Then
after values for (key1=va1) are exhausted the next distinct tuple is
searched with (key1>val1).

In short, this implementation can change the scan key structure from
"key1=val1" to "key1>val1" and back, and not just the key comparison value
(i.e. val1).
It means that so->skipScan can get reset from true to false after the next
call to _bt_preprocess_keys.

But after btrescan resets "so->numberOfKeys = 0", so->skipScan is not reset
to "false" in _bt_preprocess_keys because of this code:
https://github.com/postgres/postgres/blob/9016fa7e3bcde8ae4c3d63c707143af147486a10/src/backend/access/nbtree/nbtpreprocesskeys.c#L1847
After we set "so->numberOfKeys = 0" we quit on line 1847 before we get to
the line 1874 where we do "so->skipScan = (numSkipArrayKeys > 0);"
https://github.com/postgres/postgres/blob/9016fa7e3bcde8ae4c3d63c707143af147486a10/src/backend/access/nbtree/nbtpreprocesskeys.c#L1874

I.e. if btrescan resets "so->numberOfKeys = 0", _bt_preprocess_keys quits
before resetting so->skipScan to false.
It is not an issue when the scan key structure is not changed in amrescan,
and I see that this is an intended usage.
But in case the intended amrescan usage changes in the future, the issue
may come up.

It's not a priority at the moment as we can reset so->skipScan in our
extension.

Thank you,
Natalya Aksman.

On Wed, Sep 10, 2025 at 12:46 PM Peter Geoghegan <pg@bowt.ie> wrote:

Show quoted text

On Wed, Sep 10, 2025 at 9:53 AM Natalya Aksman <natalya@tigerdata.com>
wrote:

Our Timescaledb extension has scenarios changing ">" quals to "=" and

back on rescan and it breaks when so->Skipscan needs to be reset from true
to false.

But the amrescan docs say:

"In practice the restart feature is used when a new outer tuple is
selected by a nested-loop join and so a new key comparison value is
needed, but the scan key structure remains the same" [1].

I don't understand why it is that our not resetting the so->Skipscan
flag within btrescan has any particular significance to Timescaledb,
relative to all of the other fields that are supposed to be set by
_bt_preprocess_keys. What is the actual failure you see? Is it an
assertion failure within _bt_readpage/_bt_checkkeys?

Note that btrescan *does* set "so->numberOfKeys = 0", which will make
the next call to _bt_preprocess_keys (from _bt_first) perform
preprocessing from scratch. This should set so->Skipscan from scratch
on each rescan (along with every other field set by preprocessing). It
seems like that should work for you (in spite of the fact that you're
doing something that seems at odds with the index AM API).

[1] https://www.postgresql.org/docs/current/index-functions.html
--
Peter Geoghegan

In reply to: Natalya Aksman (#148)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Sep 10, 2025 at 2:59 PM Natalya Aksman <natalya@tigerdata.com> wrote:

But after btrescan resets "so->numberOfKeys = 0", so->skipScan is not reset to "false" in _bt_preprocess_keys because of this code: https://github.com/postgres/postgres/blob/9016fa7e3bcde8ae4c3d63c707143af147486a10/src/backend/access/nbtree/nbtpreprocesskeys.c#L1847
After we set "so->numberOfKeys = 0" we quit on line 1847 before we get to the line 1874 where we do "so->skipScan = (numSkipArrayKeys > 0);" https://github.com/postgres/postgres/blob/9016fa7e3bcde8ae4c3d63c707143af147486a10/src/backend/access/nbtree/nbtpreprocesskeys.c#L1874

It sounds like the patch that I posted fixes the problem, without you
having to set so->skipScan externally (which sounds like a big
kludge). Can you confirm that it actually does fix the problem that
you're seeing?

TimescaleDB isn't following the letter of the law here. But I do still
see the argument for consistently setting so->skipScan during
preprocessing. That at least makes sense on general robustness
grounds.

--
Peter Geoghegan

In reply to: BharatDB (#143)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Sep 10, 2025 at 2:49 AM BharatDB <bharatdbpg@gmail.com> wrote:

As a follow-up to the skip scan regression discussion, I tested a small patch that introduces static allocation/caching of `IndexAmRoutine` objects in `amapi.c`, removing the malloc/free overhead.

I think that it's too late to be considering anything this invasive for 18.

Test setup :
- Baseline: PG17 (commit before skip scan)
- After: PG18 build with skip scan (patched)
- pgbench scale=1, 100 partitions
- Query: `select count(*) from pgbench_accounts where bid = 0`
- Clients: 1, 4, 32
- Protocols: simple, prepared

Results (tps, 10s runs) :

Mode Clients Before (PG17) After (PG18 w/ static fix)

simple 1 23856 20332 (~15% lower)
simple 4 55299 53184 (~4% lower)
simple 32 79779 78347 (~2% lower)

prepared 1 26364 26615 (no regression)
prepared 4 55784 54437 (~2% lower)
prepared 32 84687 80374 (~5% lower)

This shows the static fix eliminates the severe ~50% regression previously observed by Tomas, leaving only a small residual slowdown (~2-15%).

The regression that Tomas reported is extreme and artificial. IIRC it
only affects partition queries with a hundred or so partitions, each
with an index-only scan that always scans exactly 0 index tuples, from
a pgbench_accounts that has the smallest possible amount of rows that
pgbench will allow (these are the cheapest possible index-only scans).
Plain index scans are not affected at all, presumably because it just
so happens that we don't allocate a BLCKSZ*2 workspace for plain index
scans, which is enough to put us well under the critical glibc
allocation size threshold (the threshold that the introduction of a
new nbtree support function put us over).

I also couldn't see anything like the 50% regression that Tomas
reported. And I couldn't recreate any problem unless partitioning was
used.

--
Peter Geoghegan

#151Natalya Aksman
natalya@tigerdata.com
In reply to: Peter Geoghegan (#149)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Fantastic, the patch is working, it fixes our issue!

Thank you,
Natalya Aksman.

On Wed, Sep 10, 2025 at 3:12 PM Peter Geoghegan <pg@bowt.ie> wrote:

Show quoted text

On Wed, Sep 10, 2025 at 2:59 PM Natalya Aksman <natalya@tigerdata.com>
wrote:

But after btrescan resets "so->numberOfKeys = 0", so->skipScan is not

reset to "false" in _bt_preprocess_keys because of this code:
https://github.com/postgres/postgres/blob/9016fa7e3bcde8ae4c3d63c707143af147486a10/src/backend/access/nbtree/nbtpreprocesskeys.c#L1847

After we set "so->numberOfKeys = 0" we quit on line 1847 before we get

to the line 1874 where we do "so->skipScan = (numSkipArrayKeys > 0);"
https://github.com/postgres/postgres/blob/9016fa7e3bcde8ae4c3d63c707143af147486a10/src/backend/access/nbtree/nbtpreprocesskeys.c#L1874

It sounds like the patch that I posted fixes the problem, without you
having to set so->skipScan externally (which sounds like a big
kludge). Can you confirm that it actually does fix the problem that
you're seeing?

TimescaleDB isn't following the letter of the law here. But I do still
see the argument for consistently setting so->skipScan during
preprocessing. That at least makes sense on general robustness
grounds.

--
Peter Geoghegan

In reply to: Natalya Aksman (#151)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

On Wed, Sep 10, 2025 at 3:41 PM Natalya Aksman <natalya@tigerdata.com> wrote:

Fantastic, the patch is working, it fixes our issue!

I pushed this patch just now.

Thanks
--
Peter Geoghegan

#153Natalya Aksman
natalya@tigerdata.com
In reply to: Peter Geoghegan (#152)
Re: Adding skip scan (including MDAM style range skip scan) to nbtree

Awesome, thank you for the quick turnaround.

Natalya Aksman

On Sat, Sep 13, 2025 at 9:03 PM Peter Geoghegan <pg@bowt.ie> wrote:

Show quoted text

On Wed, Sep 10, 2025 at 3:41 PM Natalya Aksman <natalya@tigerdata.com>
wrote:

Fantastic, the patch is working, it fixes our issue!

I pushed this patch just now.

Thanks
--
Peter Geoghegan